Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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