Skip to content

Commit 9e0e864

Browse files
author
Alexey Lukin
committed
PRDEV-944 #comment squashed/applied patch from FirebaseExtended#237 and added changes for copy/cut/paste buttons using same functionality
1 parent 838736f commit 9e0e864

5 files changed

Lines changed: 149 additions & 21 deletions

File tree

lib/firepad.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,32 @@ firepad.Firepad = (function (global) {
240240
return this.getHtmlFromRange(null, null);
241241
};
242242

243+
Firepad.prototype.selectionHasAttributes = function() {
244+
var startPos = this.codeMirror_.getCursor('start'), endPos = this.codeMirror_.getCursor('end');
245+
var startIndex = this.codeMirror_.indexFromPos(startPos), endIndex = this.codeMirror_.indexFromPos(endPos);
246+
return this.rangeHasAttributes(startIndex, endIndex);
247+
};
248+
249+
Firepad.prototype.rangeHasAttributes = function(start, end) {
250+
this.assertReady_('rangeHasAttributes');
251+
var doc = (start != null && end != null) ?
252+
this.getOperationForSpan(start, end) :
253+
this.getOperationForSpan(0, this.codeMirror_.getValue().length);
254+
255+
var op;
256+
for (var i = 0; i < doc.ops.length; i++) {
257+
op = doc.ops[i];
258+
for (var prop in op.attributes) {
259+
if (!op.attributes.hasOwnProperty(prop)) continue;
260+
if (prop==ATTR.LINE_SENTINEL) continue;
261+
for(var validAttr in firepad.AttributeConstants) if (firepad.AttributeConstants[validAttr] === prop) return true; // found one
262+
}
263+
}
264+
265+
return false;
266+
};
267+
268+
243269
Firepad.prototype.getHtmlFromSelection = function () {
244270
var startPos = this.codeMirror_.getCursor('start'), endPos = this.codeMirror_.getCursor('end');
245271
var startIndex = this.codeMirror_.indexFromPos(startPos), endIndex = this.codeMirror_.indexFromPos(endPos);
@@ -254,7 +280,7 @@ firepad.Firepad = (function (global) {
254280
};
255281

256282
Firepad.prototype.insertHtml = function (index, html) {
257-
var lines = firepad.ParseHtml(html, this.entityManager_);
283+
var lines = firepad.ParseHtml(html, this.entityManager_, this.codeMirror_);
258284
this.insertText(index, lines);
259285
};
260286

@@ -263,7 +289,7 @@ firepad.Firepad = (function (global) {
263289
};
264290

265291
Firepad.prototype.setHtml = function (html) {
266-
var lines = firepad.ParseHtml(html, this.entityManager_);
292+
var lines = firepad.ParseHtml(html, this.entityManager_, this.codeMirror_);
267293
this.setText(lines);
268294
};
269295

@@ -460,23 +486,20 @@ firepad.Firepad = (function (global) {
460486

461487
Firepad.prototype.cut = function () {
462488
var doc = this.codeMirror_.doc;
463-
var selection = doc.getSelection();
489+
var selection = this.editorAdapter_.rtcm.copyHtml();
464490
if (selection) {
465491
this.copyText = selection;
466492
doc.replaceSelection('');
467493
}
468494
};
469495
Firepad.prototype.copy = function () {
470-
var doc = this.codeMirror_.doc;
471-
var selection = doc.getSelection();
496+
var selection = this.editorAdapter_.rtcm.copyHtml();
472497
if (selection) {
473498
this.copyText = selection;
474499
}
475500
};
476501
Firepad.prototype.paste = function () {
477-
var cm = this.codeMirror_,
478-
index = cm.indexFromPos(cm.getCursor('head'));
479-
this.richTextCodeMirror_.insertText(index, this.copyText);
502+
this.insertHtmlAtCursor(this.copyText);
480503
};
481504

482505
Firepad.prototype.markQuickmark = function (id) {

lib/headless.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ firepad.Headless = (function() {
9292
var self = this;
9393

9494
self.initializeFakeDom(function() {
95-
var textPieces = ParseHtml(html, self.entityManager);
95+
var textPieces = ParseHtml(html, self.entityManager_, self.codeMirror_);
9696
var inserts = firepad.textPiecesToInserts(true, textPieces);
9797
var op = new TextOperation();
9898

lib/parse-html.js

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ firepad.ParseHtml = (function () {
5555
}
5656

5757
ParseOutput.prototype.newlineIfNonEmpty = function(state) {
58-
this.cleanLine_();
58+
this.cleanLine_(true);
5959
if (this.currentLine.length > 0) {
6060
this.newline(state);
6161
}
6262
};
6363

6464
ParseOutput.prototype.newlineIfNonEmptyOrListItem = function(state) {
65-
this.cleanLine_();
65+
this.cleanLine_(true);
6666
if (this.currentLine.length > 0 || this.currentLineListItemType !== null) {
6767
this.newline(state);
6868
}
@@ -84,15 +84,17 @@ firepad.ParseHtml = (function () {
8484
this.currentLineListItemType = type;
8585
};
8686

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

104-
var entityManager_;
105-
function parseHtml(html, entityManager) {
106+
var entityManager_, codeMirror_;
107+
function parseHtml(html, entityManager, codeMirror) {
108+
html=html.replace(/(\r\n|\n|\r)?<html>(\r\n|\n|\r)?<body>(\r\n|\n|\r)?/, ''); // remove <html><body>
109+
html=html.replace(/(\r\n|\n|\r)?<\/body>(\r\n|\n|\r)?<\/html>(\r\n|\n|\r)?/, ''); // remove </body></html>
110+
106111
// Create DIV with HTML (as a convenient way to parse it).
107112
var div = (firepad.document || document).createElement('div');
108113
div.innerHTML = html;
109114

110115
// HACK until I refactor this.
111116
entityManager_ = entityManager;
117+
codeMirror_ = codeMirror;
112118

113119
var output = new ParseOutput();
114120
var state = new ParseState();
@@ -138,8 +144,8 @@ firepad.ParseHtml = (function () {
138144

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

249+
function styleEqual(s1,s2) {
250+
s1=s1.toLowerCase(); // lower
251+
s1=s1.split(' ').join(''); // remove spaces
252+
s1=s1.lastIndexOf(";") == s1.length - 1 ? s1.substring(0, s1.length -1 ) : s1; // remove trailing ;
253+
s2=s2.toLowerCase(); // lower
254+
s2=s2.split(' ').join(''); // remove spaces
255+
s2=s2.lastIndexOf(";") == s2.length - 1 ? s2.substring(0, s2.length -1 ) : s2; // remove trailing ;
256+
return s1==s2;
257+
}
258+
243259
function parseStyle(state, styleString) {
260+
if (!this.firepadDefaultStyles) {
261+
// caching some default styles needed later
262+
var style = window.getComputedStyle(codeMirror_.getWrapperElement());
263+
firepadDefaultStyles={
264+
fontFamily: style.getPropertyValue('font-family'),
265+
fontSize: style.getPropertyValue('font-size'),
266+
backgroundColor: style.getPropertyValue('background-color'),
267+
color: style.getPropertyValue('color'),
268+
textAlign: style.getPropertyValue('text-align')
269+
};
270+
}
271+
244272
var textFormatting = state.textFormatting;
245273
var lineFormatting = state.lineFormatting;
246274
var styles = styleString.split(';');
@@ -265,6 +293,7 @@ firepad.ParseHtml = (function () {
265293
textFormatting = textFormatting.italic(italic);
266294
break;
267295
case 'color':
296+
if (styleEqual(val, this.firepadDefaultStyles.color)) break;
268297
textFormatting = textFormatting.color(val);
269298
break;
270299
case 'qmid':
@@ -274,12 +303,15 @@ firepad.ParseHtml = (function () {
274303
textFormatting = textFormatting.qmclass();
275304
break;
276305
case 'background-color':
306+
if (styleEqual(val, this.firepadDefaultStyles.backgroundColor)) break;
277307
textFormatting = textFormatting.backgroundColor(val);
278308
break;
279309
case 'text-align':
310+
if (styleEqual(val, this.firepadDefaultStyles.textAlign)) break;
280311
lineFormatting = lineFormatting.align(val);
281312
break;
282313
case 'font-size':
314+
if (styleEqual(val, this.firepadDefaultStyles.fontSize)) break;
283315
var size = null;
284316
var allowedValues = ['px','pt','%','em','xx-small','x-small','small','medium','large','x-large','xx-large','smaller','larger'];
285317
if (firepad.utils.stringEndsWith(val, allowedValues)) {
@@ -293,6 +325,7 @@ firepad.ParseHtml = (function () {
293325
}
294326
break;
295327
case 'font-family':
328+
if (styleEqual(val, this.firepadDefaultStyles.fontFamily)) break;
296329
var font = firepad.utils.trim(val.split(',')[0]); // get first font.
297330
font = font.replace(/['"]/g, ''); // remove quotes.
298331
font = font.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() });

lib/rich-text-codemirror.js

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,21 @@ firepad.RichTextCodeMirror = (function () {
3636
bind(this, 'onCodeMirrorChange_');
3737
bind(this, 'onCursorActivity_');
3838

39-
if (/^4\./.test(CodeMirror.version)) {
39+
bind(this, 'onCodeMirrorCopyCut_');
40+
bind(this, 'onCodeMirrorPaste_');
41+
42+
if (parseInt(CodeMirror.version) >= 4) {
4043
this.codeMirror.on('changes', this.onCodeMirrorChange_);
4144
} else {
4245
this.codeMirror.on('change', this.onCodeMirrorChange_);
4346
}
4447
this.codeMirror.on('beforeChange', this.onCodeMirrorBeforeChange_);
4548
this.codeMirror.on('cursorActivity', this.onCursorActivity_);
4649

50+
this.codeMirror.on('copy', this.onCodeMirrorCopyCut_);
51+
this.codeMirror.on('cut', this.onCodeMirrorCopyCut_);
52+
this.codeMirror.on('paste', this.onCodeMirrorPaste_);
53+
4754
this.changeId_ = 0;
4855
this.outstandingChanges_ = { };
4956
this.dirtyLines_ = [];
@@ -59,6 +66,11 @@ firepad.RichTextCodeMirror = (function () {
5966
this.codeMirror.off('change', this.onCodeMirrorChange_);
6067
this.codeMirror.off('changes', this.onCodeMirrorChange_);
6168
this.codeMirror.off('cursorActivity', this.onCursorActivity_);
69+
70+
this.codeMirror.off('copy', this.onCodeMirrorCopyCut_);
71+
this.codeMirror.off('cut', this.onCodeMirrorCopyCut_);
72+
this.codeMirror.off('paste', this.onCodeMirrorPaste_);
73+
6274
this.clearAnnotations_();
6375
};
6476

@@ -581,6 +593,65 @@ firepad.RichTextCodeMirror = (function () {
581593
}
582594
};
583595

596+
RichTextCodeMirror.prototype.copyHtml = function() {
597+
// one time caching of html styles
598+
if ( !this.firepadStyleWrapper ) {
599+
var style = window.getComputedStyle(this.codeMirror.getWrapperElement());
600+
this.firepadStyleWrapper =
601+
'font-family:' + style.getPropertyValue('font-family') + ';' +
602+
'font-size:' + style.getPropertyValue('font-size') + ';' +
603+
'background-color:' + style.getPropertyValue('background-color') + ';' +
604+
'color:' + style.getPropertyValue('color') + ';' +
605+
'text-align:' + style.getPropertyValue('text-align') + ';';
606+
}
607+
608+
var fp = this.codeMirror.firepad;
609+
var val = '';
610+
611+
// if selection has attributes try to get html
612+
if ( fp.selectionHasAttributes() ) {
613+
val = fp.getHtmlFromSelection();
614+
if ( val ) {
615+
val = '<span style="' + this.firepadStyleWrapper + '">' + val + '</span>';
616+
}
617+
}
618+
619+
return val;
620+
};
621+
622+
RichTextCodeMirror.prototype.onCodeMirrorCopyCut_ = function( cm, e ) {
623+
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
624+
if ( !e.clipboardData || ios ) return; // clipboard ops not supported
625+
626+
var gotHtml = false,
627+
val = this.copyHtml();
628+
if (val) {
629+
gotHtml = true;
630+
}
631+
// if we couldn't get html try to get text
632+
else {
633+
// remove sentinels
634+
val = this.codeMirror.getSelections().join('\n').replace(new RegExp('[' + LineSentinelCharacter + EntitySentinelCharacter + ']', 'g'), '');
635+
// no html or text - something went wrong
636+
if ( !val ) return;
637+
}
638+
639+
if ( e.type == 'cut' ) cm.replaceSelection('', null, 'cut');
640+
e.clipboardData.clearData();
641+
e.clipboardData.setData(gotHtml ? 'text/html' : 'text', val);
642+
e.preventDefault()
643+
};
644+
645+
RichTextCodeMirror.prototype.onCodeMirrorPaste_ = function( cm, e ) {
646+
var html = e.clipboardData ? e.clipboardData.getData('text/html') : null;
647+
if ( !html ) return; // not html or something went wrong, revert to CM paste
648+
649+
cm.replaceSelection('');
650+
var fp = this.codeMirror.firepad;
651+
fp.insertHtmlAtCursor(html);
652+
e.preventDefault();
653+
};
654+
584655
function cmpPos (a, b) {
585656
return (a.line - b.line) || (a.ch - b.ch);
586657
}
@@ -1153,7 +1224,7 @@ firepad.RichTextCodeMirror = (function () {
11531224
function bind (obj, method) {
11541225
var fn = obj[method];
11551226
obj[method] = function () {
1156-
fn.apply(obj, arguments);
1227+
return fn.apply(obj, arguments);
11571228
};
11581229
}
11591230

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"karma-coverage": "^0.2.6",
5555
"karma-failed-reporter": "0.0.2",
5656
"karma-jasmine": "~0.1.3",
57-
"karma-phantomjs-launcher": "~0.1.0",
57+
"phantomjs-prebuilt": "2.1.4",
58+
"karma-phantomjs-launcher": "~1.0.0",
5859
"karma-spec-reporter": "0.0.13"
5960
},
6061
"scripts": {

0 commit comments

Comments
 (0)