Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Rich text copy/cut/paste #237

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/firepad.js
Original file line number Diff line number Diff line change
Expand Up @@ -5868,4 +5868,4 @@ firepad.Firepad.Headless = firepad.Headless;
firepad.Firepad.RichTextCodeMirrorAdapter = firepad.RichTextCodeMirrorAdapter;
firepad.Firepad.ACEAdapter = firepad.ACEAdapter;

return firepad.Firepad; }, this);
return firepad.Firepad; }, this);
30 changes: 28 additions & 2 deletions lib/firepad.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,32 @@ firepad.Firepad = (function(global) {
return this.getHtmlFromRange(null, null);
};

Firepad.prototype.selectionHasAttributes = function() {
var startPos = this.codeMirror_.getCursor('start'), endPos = this.codeMirror_.getCursor('end');
var startIndex = this.codeMirror_.indexFromPos(startPos), endIndex = this.codeMirror_.indexFromPos(endPos);
return this.rangeHasAttributes(startIndex, endIndex);
};

Firepad.prototype.rangeHasAttributes = function(start, end) {
this.assertReady_('rangeHasAttributes');
var doc = (start != null && end != null) ?
this.getOperationForSpan(start, end) :
this.getOperationForSpan(0, this.codeMirror_.getValue().length);

var op;
for (var i = 0; i < doc.ops.length; i++) {
op = doc.ops[i];
for (var prop in op.attributes) {
if (!op.attributes.hasOwnProperty(prop)) continue;
if (prop==ATTR.LINE_SENTINEL) continue;
for(var validAttr in firepad.AttributeConstants) if (firepad.AttributeConstants[validAttr] === prop) return true; // found one
}
}

return false;
};


Firepad.prototype.getHtmlFromSelection = function() {
var startPos = this.codeMirror_.getCursor('start'), endPos = this.codeMirror_.getCursor('end');
var startIndex = this.codeMirror_.indexFromPos(startPos), endIndex = this.codeMirror_.indexFromPos(endPos);
Expand All @@ -250,7 +276,7 @@ firepad.Firepad = (function(global) {
};

Firepad.prototype.insertHtml = function (index, html) {
var lines = firepad.ParseHtml(html, this.entityManager_);
var lines = firepad.ParseHtml(html, this.entityManager_, this.codeMirror_);
this.insertText(index, lines);
};

Expand All @@ -259,7 +285,7 @@ firepad.Firepad = (function(global) {
};

Firepad.prototype.setHtml = function (html) {
var lines = firepad.ParseHtml(html, this.entityManager_);
var lines = firepad.ParseHtml(html, this.entityManager_, this.codeMirror_);
this.setText(lines);
};

Expand Down
2 changes: 1 addition & 1 deletion lib/headless.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ firepad.Headless = (function() {
}

self.initializeFakeDom(function() {
var textPieces = ParseHtml(html, self.entityManager_);
var textPieces = ParseHtml(html, self.entityManager_, self.codeMirror_);
var inserts = firepad.textPiecesToInserts(true, textPieces);
var op = new TextOperation();

Expand Down
51 changes: 42 additions & 9 deletions lib/parse-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ firepad.ParseHtml = (function () {
}

ParseOutput.prototype.newlineIfNonEmpty = function(state) {
this.cleanLine_();
this.cleanLine_(true);
if (this.currentLine.length > 0) {
this.newline(state);
}
};

ParseOutput.prototype.newlineIfNonEmptyOrListItem = function(state) {
this.cleanLine_();
this.cleanLine_(true);
if (this.currentLine.length > 0 || this.currentLineListItemType !== null) {
this.newline(state);
}
Expand All @@ -84,15 +84,17 @@ firepad.ParseHtml = (function () {
this.currentLineListItemType = type;
};

ParseOutput.prototype.cleanLine_ = function() {
ParseOutput.prototype.cleanLine_ = function(ignoreNbsps) {
// Kinda' a hack, but we remove leading and trailing spaces (since these aren't significant in html) and
// replaces nbsp's with normal spaces.
if (this.currentLine.length > 0) {
var last = this.currentLine.length - 1;
this.currentLine[0].text = this.currentLine[0].text.replace(/^ +/, '');
this.currentLine[last].text = this.currentLine[last].text.replace(/ +$/g, '');
for(var i = 0; i < this.currentLine.length; i++) {
this.currentLine[i].text = this.currentLine[i].text.replace(/\u00a0/g, ' ');
if (!ignoreNbsps) {
for(var i = 0; i < this.currentLine.length; i++) {
this.currentLine[i].text = this.currentLine[i].text.replace(/\u00a0/g, ' ');
}
}
}
// If after stripping trailing whitespace, there's nothing left, clear currentLine out.
Expand All @@ -101,14 +103,18 @@ firepad.ParseHtml = (function () {
}
};

var entityManager_;
function parseHtml(html, entityManager) {
var entityManager_, codeMirror_;
function parseHtml(html, entityManager, codeMirror) {
html=html.replace(/(\r\n|\n|\r)?<html>(\r\n|\n|\r)?<body>(\r\n|\n|\r)?/, ''); // remove <html><body>
html=html.replace(/(\r\n|\n|\r)?<\/body>(\r\n|\n|\r)?<\/html>(\r\n|\n|\r)?/, ''); // remove </body></html>

// Create DIV with HTML (as a convenient way to parse it).
var div = (firepad.document || document).createElement('div');
div.innerHTML = html;

// HACK until I refactor this.
entityManager_ = entityManager;
codeMirror_ = codeMirror;

var output = new ParseOutput();
var state = new ParseState();
Expand Down Expand Up @@ -138,8 +144,8 @@ firepad.ParseHtml = (function () {

switch (node.nodeType) {
case Node.TEXT_NODE:
// This probably isn't exactly right, but mostly works...
var text = node.nodeValue.replace(/[ \n\t]+/g, ' ');
// replace spaces with &nbsp; so they can withstand cleanLine_
var text = node.nodeValue.replace(/ /g, '\u00a0');
output.currentLine.push(firepad.Text(text, state.textFormatting));
break;
case Node.ELEMENT_NODE:
Expand Down Expand Up @@ -240,7 +246,29 @@ firepad.ParseHtml = (function () {
}
}

function styleEqual(s1,s2) {
s1=s1.toLowerCase(); // lower
s1=s1.split(' ').join(''); // remove spaces
s1=s1.lastIndexOf(";") == s1.length - 1 ? s1.substring(0, s1.length -1 ) : s1; // remove trailing ;
s2=s2.toLowerCase(); // lower
s2=s2.split(' ').join(''); // remove spaces
s2=s2.lastIndexOf(";") == s2.length - 1 ? s2.substring(0, s2.length -1 ) : s2; // remove trailing ;
return s1==s2;
}

function parseStyle(state, styleString) {
if (!this.firepadDefaultStyles) {
// caching some default styles needed later
var style = window.getComputedStyle(codeMirror_.getWrapperElement());
firepadDefaultStyles={
fontFamily: style.getPropertyValue('font-family'),
fontSize: style.getPropertyValue('font-size'),
backgroundColor: style.getPropertyValue('background-color'),
color: style.getPropertyValue('color'),
textAlign: style.getPropertyValue('text-align')
};
}

var textFormatting = state.textFormatting;
var lineFormatting = state.lineFormatting;
var styles = styleString.split(';');
Expand All @@ -265,15 +293,19 @@ firepad.ParseHtml = (function () {
textFormatting = textFormatting.italic(italic);
break;
case 'color':
if (styleEqual(val, this.firepadDefaultStyles.color)) break;
textFormatting = textFormatting.color(val);
break;
case 'background-color':
if (styleEqual(val, this.firepadDefaultStyles.backgroundColor)) break;
textFormatting = textFormatting.backgroundColor(val);
break;
case 'text-align':
if (styleEqual(val, this.firepadDefaultStyles.textAlign)) break;
lineFormatting = lineFormatting.align(val);
break;
case 'font-size':
if (styleEqual(val, this.firepadDefaultStyles.fontSize)) break;
var size = null;
var allowedValues = ['px','pt','%','em','xx-small','x-small','small','medium','large','x-large','xx-large','smaller','larger'];
if (firepad.utils.stringEndsWith(val, allowedValues)) {
Expand All @@ -287,6 +319,7 @@ firepad.ParseHtml = (function () {
}
break;
case 'font-family':
if (styleEqual(val, this.firepadDefaultStyles.fontFamily)) break;
var font = firepad.utils.trim(val.split(',')[0]); // get first font.
font = font.replace(/['"]/g, ''); // remove quotes.
font = font.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() });
Expand Down
46 changes: 45 additions & 1 deletion lib/rich-text-codemirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ firepad.RichTextCodeMirror = (function () {
bind(this, 'onCodeMirrorChange_');
bind(this, 'onCursorActivity_');

bind(this, 'onCodeMirrorCopyCut_');
bind(this, 'onCodeMirrorPaste_');

if (parseInt(CodeMirror.version) >= 4) {
this.codeMirror.on('changes', this.onCodeMirrorChange_);
} else {
Expand All @@ -44,6 +47,10 @@ firepad.RichTextCodeMirror = (function () {
this.codeMirror.on('beforeChange', this.onCodeMirrorBeforeChange_);
this.codeMirror.on('cursorActivity', this.onCursorActivity_);

this.codeMirror.on('copy', this.onCodeMirrorCopyCut_);
this.codeMirror.on('cut', this.onCodeMirrorCopyCut_);
this.codeMirror.on('paste', this.onCodeMirrorPaste_);

this.changeId_ = 0;
this.outstandingChanges_ = { };
this.dirtyLines_ = [];
Expand All @@ -59,6 +66,11 @@ firepad.RichTextCodeMirror = (function () {
this.codeMirror.off('change', this.onCodeMirrorChange_);
this.codeMirror.off('changes', this.onCodeMirrorChange_);
this.codeMirror.off('cursorActivity', this.onCursorActivity_);

this.codeMirror.off('copy', this.onCodeMirrorCopyCut_);
this.codeMirror.off('cut', this.onCodeMirrorCopyCut_);
this.codeMirror.off('paste', this.onCodeMirrorPaste_);

this.clearAnnotations_();
};

Expand Down Expand Up @@ -570,6 +582,38 @@ firepad.RichTextCodeMirror = (function () {
}
};

RichTextCodeMirror.prototype.onCodeMirrorCopyCut_ = function(cm, e) {
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
if (!e.clipboardData || ios) return; // clipboard ops not supported

var fp=this.codeMirror.firepad;

let textVal=this.codeMirror.getSelections().join('\n').replace(new RegExp('[' + LineSentinelCharacter + EntitySentinelCharacter + ']', 'g'), ''); // remove sentinels
if (!textVal) return; // something went wrong
//utils.log(textVal);

var htmlVal;
if (fp.selectionHasAttributes()) htmlVal=fp.getHtmlFromSelection();
//if (htmlVal) utils.log(htmlVal);

if (e.type == 'cut') cm.replaceSelection('', null, 'cut');
e.clipboardData.clearData();
e.clipboardData.setData('text', textVal);
if (htmlVal) e.clipboardData.setData('text/html', htmlVal);
e.preventDefault()
};

RichTextCodeMirror.prototype.onCodeMirrorPaste_ = function(cm, e) {
var html = e.clipboardData ? e.clipboardData.getData('text/html') : null;
if (!html) return; // not html or something went wrong, revert to CM paste

cm.replaceSelection('');
var fp=this.codeMirror.firepad;
fp.insertHtmlAtCursor(html);
e.preventDefault();
//utils.log(html);
};

function cmpPos (a, b) {
return (a.line - b.line) || (a.ch - b.ch);
}
Expand Down Expand Up @@ -1137,7 +1181,7 @@ firepad.RichTextCodeMirror = (function () {
function bind (obj, method) {
var fn = obj[method];
obj[method] = function () {
fn.apply(obj, arguments);
return fn.apply(obj, arguments);
};
}

Expand Down
6 changes: 1 addition & 5 deletions lib/serialize-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,6 @@ firepad.SerializeHtml = (function () {
op = doc.ops[++i];
}

// Replace leading, trailing, and consecutive spaces with nbsp's to make sure they're preserved.
text = text.replace(/ +/g, function(str) {
return new Array(str.length + 1).join('\u00a0');
}).replace(/^ /, '\u00a0').replace(/ $/, '\u00a0');
if (text.length > 0) {
emptyLine = false;
}
Expand All @@ -197,7 +193,7 @@ firepad.SerializeHtml = (function () {
html = TODO_STYLE + html;
}

return html;
return '<pre>'+html+'</pre>';
}

return serializeHtml;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"karma-coverage": "^0.2.6",
"karma-failed-reporter": "0.0.2",
"karma-jasmine": "^0.3.6",
"karma-phantomjs-launcher": "~0.1.0",
"phantomjs-prebuilt": "2.1.4",
"karma-phantomjs-launcher": "~1.0.0",
"karma-spec-reporter": "0.0.13"
},
"scripts": {
Expand Down