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 11 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
116 changes: 108 additions & 8 deletions examples/firepad.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* it requires no server-side code and can be added to any web app simply by
* including a couple JavaScript files.
*
* Firepad 0.0.0
* Firepad 1.3.0
* http://www.firepad.io/
* License: MIT
* Copyright: 2014 Firebase
Expand Down Expand Up @@ -2923,6 +2923,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 @@ -2931,6 +2934,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 @@ -2946,6 +2953,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 @@ -3457,6 +3469,44 @@ firepad.RichTextCodeMirror = (function () {
}
};

RichTextCodeMirror.prototype.onCodeMirrorCopyCut_ = function(cm, e) {
var fp=this.codeMirror.firepad;
if (!fp.selectionHasAttributes()) return ; // not rich text

var html=fp.getHtmlFromSelection();
if (!html) return; // something went wrong

if (!this.firepadStyleWrapper) {
var style = window.getComputedStyle(this.codeMirror.getWrapperElement());
this.firepadStyleWrapper=
'font-family:'+style.getPropertyValue('font-family')+';'+
'font-size:'+style.getPropertyValue('font-size')+';'+
'background-color:'+style.getPropertyValue('background-color')+';'+
'color:'+style.getPropertyValue('color')+';'+
'text-align:'+style.getPropertyValue('text-align')+';';
}
html='<span style="'+this.firepadStyleWrapper+'">'+html+'</span>';

var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
if (!e.clipboardData || ios) return; // clipboard ops not supported

if (e.type == "cut") cm.replaceSelection("", null, "cut");

e.preventDefault();
e.clipboardData.clearData();
e.clipboardData.setData("text/html", html);
};

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

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

function cmpPos (a, b) {
return (a.line - b.line) || (a.ch - b.ch);
}
Expand Down Expand Up @@ -4024,7 +4074,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 Expand Up @@ -4733,14 +4783,15 @@ firepad.ParseHtml = (function () {
}
};

var entityManager_;
function parseHtml(html, entityManager) {
var entityManager_, codeMirror_;
function parseHtml(html, entityManager, codeMirror) {
// 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 @@ -4872,7 +4923,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 @@ -4897,15 +4970,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 @@ -4919,6 +4996,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 Expand Up @@ -5293,7 +5371,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 Expand Up @@ -5568,6 +5646,28 @@ 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) && prop!=ATTR.LINE_SENTINEL) return true; // found an attribute
}

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 @@ -5583,7 +5683,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 @@ -5592,7 +5692,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 Expand Up @@ -5868,4 +5968,4 @@ firepad.Firepad.Headless = firepad.Headless;
firepad.Firepad.RichTextCodeMirrorAdapter = firepad.RichTextCodeMirrorAdapter;
firepad.Firepad.ACEAdapter = firepad.ACEAdapter;

return firepad.Firepad; }, this);
return firepad.Firepad; }, this);
27 changes: 24 additions & 3 deletions lib/firepad.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ firepad.Firepad = (function(global) {
}
this.client_ = new EditorClient(this.firebaseAdapter_, this.editorAdapter_);

var self = this;
this.firebaseAdapter_.on('cursor', function() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleting this definition of 'self' breaks a lot of things later on

self.trigger.apply(self, ['cursor'].concat([].slice.call(arguments)));
});
Expand Down Expand Up @@ -235,6 +234,28 @@ 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) && prop!=ATTR.LINE_SENTINEL) return true; // found an attribute
}

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 +271,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 +280,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
32 changes: 30 additions & 2 deletions lib/parse-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,15 @@ firepad.ParseHtml = (function () {
}
};

var entityManager_;
function parseHtml(html, entityManager) {
var entityManager_, codeMirror_;
function parseHtml(html, entityManager, codeMirror) {
// 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 @@ -240,7 +241,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 +288,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 +314,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
Loading