MooEditable.Selection

28 views
Skip to first unread message

ryan

unread,
Jan 31, 2009, 7:37:04 PM1/31/09
to MooEditable
Hello

As i mentioned, i found a few quirks in MooEditable.Selection,
particularly with webkit - it was returning the selected node as
<body> even when the getContent method was returning the correct
value.

Anyway, after a bit of hoking, i noticed that the guys at 37 signals
are creating a wyswig editor to work with prototype, and i checked out
their selection class - it seemed to have a lot more webkit specific
stuff in it, so i ported it over to MooEditable and my problems were
gone...

Here's the new class - I've not noticed any impact on other
browsers.... so it seems to be a positive change.

MooEditable.Selection = new Class({

initialize: function(editor) {
this.win = editor.win;
this.doc = editor.doc;
},

createRangeFromElement: function(node) {
if (this.doc.body.createTextRange) {
var range = this.doc.body.createTextRange();
range.moveToElementText(node);
} else if (this.doc.createRange) {
var range = this.doc.createRange();
range.selectNodeContents(node);
}
return range;
},

compareRanges: function(r1, r2) {
if (r1.compareEndPoints) {
return !(
r2.compareEndPoints('StartToStart', r1) == 1 &&
r2.compareEndPoints('EndToEnd', r1) == 1 &&
r2.compareEndPoints('StartToEnd', r1) == 1 &&
r2.compareEndPoints('EndToStart', r1) == 1
||
r2.compareEndPoints('StartToStart', r1) == -1 &&
r2.compareEndPoints('EndToEnd', r1) == -1 &&
r2.compareEndPoints('StartToEnd', r1) == -1 &&
r2.compareEndPoints('EndToStart', r1) == -1
);
} else if (r1.compareBoundaryPoints) {
return !(
r2.compareBoundaryPoints(0, r1) == 1 &&
r2.compareBoundaryPoints(2, r1) == 1 &&
r2.compareBoundaryPoints(1, r1) == 1 &&
r2.compareBoundaryPoints(3, r1) == 1
||
r2.compareBoundaryPoints(0, r1) == -1 &&
r2.compareBoundaryPoints(2, r1) == -1 &&
r2.compareBoundaryPoints(1, r1) == -1 &&
r2.compareBoundaryPoints(3, r1) == -1
);
}

return null;
},

getSelection: function() {
return (this.win.getSelection) ? this.win.getSelection() :
this.doc.selection;
},

getRange: function() {
var selection = this.getSelection();

try {
var range;
if (selection.getRangeAt)
range = selection.getRangeAt(0);
else
range = selection.createRange();
} catch(e) { return null; }

if (Browser.Engine.WebKit) {
range.setStart(selection.baseNode, selection.baseOffset);
range.setEnd(selection.extentNode, selection.extentOffset);
}

return range;
},

/*
getRange: function() {
var s = this.getSelection();

if (!s) return null;

try {

range = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ?
s.createRange() : null);

if (Browser.Engine.WebKit) {
range.setStart(selection.baseNode, selection.baseOffset);
range.setEnd(selection.extentNode, selection.extentOffset);
}

return range;


} catch (e) {
// IE bug when used in frameset
return this.doc.body.createTextRange();
}
}, */

setRange: function(range) {
if (range.select) $try(function(){
range.select();
});
else {
var s = this.getSelection();
if (s.addRange) {
s.removeAllRanges();
s.addRange(range);
}
}
},

selectNode: function(node) {
var selection = this.getSelection();

if (Browser.Engine.Trident) {
var range = createRangeFromElement(node);
range.select();
} else if (Browser.Engine.WebKit) {
selection.setBaseAndExtent(node, 0, node, node.innerText.length);
} else if (Browser.Engine.Presto) {
range = this.doc.createRange();
range.selectNode(node);
selection.removeAllRanges();
selection.addRange(range);
} else {
var range = this.createRangeFromElement(node);
selection.removeAllRanges();
selection.addRange(range);
}

return node;
},

isCollapsed: function() {
var r = this.getRange();
if (r.item) return false;
return r.boundingWidth == 0 || this.getSelection().isCollapsed;
},

collapse: function(toStart) {
var r = this.getRange();
var s = this.getSelection();

if (r.select) {
r.collapse(toStart);
r.select();
}
else
toStart ? s.collapseToStart() : s.collapseToEnd();
},

getContent: function() {
var r = this.getRange();
var body = new Element('body');

if (this.isCollapsed()) return '';

if (r.cloneContents) body.appendChild(r.cloneContents());
else if ($defined(r.item) || $defined(r.htmlText)) body.set('html',
r.item ? r.item(0).outerHTML : r.htmlText);
else body.set('html', r.toString());

var content = body.get('html');
return content;
},

getText : function() {
var r = this.getRange();
var s = this.getSelection();

return this.isCollapsed() ? '' : r.text || s.toString();
},

getNode: function() {
var nodes = null, candidates = [], children, el;
var range = this.getRange();

if (!range)
return null;

var parent;
if (range.parentElement)
parent = range.parentElement();
else
parent = range.commonAncestorContainer;

if (parent) {
while (parent.nodeType != 1) parent = parent.parentNode;
if (parent.nodeName.toLowerCase() != "body") {
el = parent;
do {
el = el.parentNode;
candidates[candidates.length] = el;
} while (el.nodeName.toLowerCase() != "body");
}
children = parent.all || parent.getElementsByTagName("*");
for (var j = 0; j < children.length; j++)
candidates[candidates.length] = children[j];
nodes = [parent];
for (var ii = 0, r2; ii < candidates.length; ii++) {
r2 = this.createRangeFromElement(candidates[ii]);
if (r2 && this.compareRanges(range, r2))
nodes[nodes.length] = candidates[ii];
}
}

return $(nodes[0]);
},

insertContent: function(content) {
var r = this.getRange();

if (r.insertNode) {
r.deleteContents();
if($type(content) == 'element') {
var temp = new Element('div').adopt(content);
content = temp.get('html');
}

r.insertNode(r.createContextualFragment(content));
}
else {
// Handle text and control range
if (r.pasteHTML) r.pasteHTML(content);
else r.item(0).outerHTML = content;
}
},

// get the properties of attributes
// return an object/hash
getProperties: function(me,tag,props) {

var el = me.selection.getNode();
var vals = new Hash();

if (el) {
if (el.nodeType == 1) {
if (tag.toLowerCase() == el.tagName.toLowerCase()) {
var hsh = new Hash({});
props.each(function(e) {
if(el.getProperty(e) != null)
hsh.set(e,el.getProperty(e))
},this);
return hsh;
}
}
}

return vals;

},

// surround selection with element (same as outerHTML)
surround: function(el) {
this.insertContent(el.set('html',this.getRange()));
}

});

cheeaun

unread,
Jan 31, 2009, 9:53:32 PM1/31/09
to MooEditable
Great. Glad you find the quirk ;)

The current getRange function is actually a copy from PunyMCE's
Selection class:
http://code.google.com/p/punymce/source/browse/trunk/js/punymce/puny_mce_src.js#1266
.. which is kinda the shorter version of TinyMCE's:
http://tinymce.svn.sourceforge.net/viewvc/tinymce/tinymce/trunk/jscripts/tiny_mce/tiny_mce_src.js?view=markup#l_4022

I read 37signals wysihat source before and notice the webkit-specific
code as well, but don't really 'get it'. I guess we'll use some stuff
from their source as well.

ryan

unread,
Feb 1, 2009, 5:28:28 AM2/1/09
to MooEditable
Yeah it all seemed pretty similar apart from the range calculation -
basically my problem was with a <div> element, when you made it the
selected node, it would always say that <body> was the selected node
(in WebKit - firefox always returned the correct node) - the wysihat
range stuff seemed to fix that.

Ryan
Reply all
Reply to author
Forward
0 new messages