MDL-44219 Atto: Convert selection logic to use Rangy JS Library

This removes alot of complex, hard to test, hard to debug code from the editor so
we can focus on the things we want to spend time on. Rangy seems a good choice for
browser support, stability and features.
This commit is contained in:
Damyon Wiese
2014-03-26 10:01:48 +08:00
parent 67d3fe45bb
commit d321f68b3d
25 changed files with 13767 additions and 1300 deletions
@@ -81,7 +81,7 @@ M.atto_equation = M.atto_equation || {
M.editor_atto.focus(elementid);
}
M.atto_equation.selection = M.editor_atto.get_selection();
if (M.atto_equation.selection !== false && (!M.atto_equation.selection.collapsed)) {
if (M.atto_equation.selection) {
var dialogue;
if (!M.atto_equation.dialogue) {
dialogue = new M.core.dialogue({
File diff suppressed because one or more lines are too long
@@ -81,7 +81,7 @@ M.atto_equation = M.atto_equation || {
M.editor_atto.focus(elementid);
}
M.atto_equation.selection = M.editor_atto.get_selection();
if (M.atto_equation.selection !== false && (!M.atto_equation.selection.collapsed)) {
if (M.atto_equation.selection) {
var dialogue;
if (!M.atto_equation.dialogue) {
dialogue = new M.core.dialogue({
@@ -79,7 +79,7 @@ M.atto_equation = M.atto_equation || {
M.editor_atto.focus(elementid);
}
M.atto_equation.selection = M.editor_atto.get_selection();
if (M.atto_equation.selection !== false && (!M.atto_equation.selection.collapsed)) {
if (M.atto_equation.selection) {
var dialogue;
if (!M.atto_equation.dialogue) {
dialogue = new M.core.dialogue({
@@ -145,6 +145,7 @@ M.atto_image = M.atto_image || {
dialogue: null,
selection: null,
currentlyselected : {},
lastselectedimage : null,
init: function(params) {
var display_chooser = function(e, elementid) {
e.preventDefault();
@@ -183,7 +184,7 @@ M.atto_image = M.atto_image || {
// This includes cursor changes, we check if the button should be active or not, based
// on the text selection.
M.editor_atto.on('atto:selectionchanged', function(e) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes)) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes, false)) {
M.editor_atto.add_widget_highlight(e.elementid, 'image');
M.editor_atto.currentlyselected[e.elementid] = e.selectedNodes;
} else {
@@ -263,8 +264,13 @@ M.atto_image = M.atto_image || {
M.atto_image.dialogue.hide();
M.editor_atto.focus(elementid);
if (url !== '') {
M.editor_atto.set_selection(M.atto_image.selection);
if (this.lastselectedimage) {
M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage));
} else {
M.editor_atto.set_selection(M.atto_image.selection);
}
imagehtml = '<img src="' + Y.Escape.html(url) + '" alt="' + Y.Escape.html(alt) + '"';
if (width) {
@@ -310,14 +316,14 @@ M.atto_image = M.atto_image || {
images = M.editor_atto.currentlyselected[elementid],
i, image, width, height, style;
if (images !== null && images.size() > 0) {
if (images) {
images = images.filter('img');
}
if (images && images.size()) {
image = images.item(0);
for (i in images) {
if (images[i].test('img')) {
image = images[i];
break;
}
}
this.lastselectedimage = image;
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
File diff suppressed because one or more lines are too long
@@ -145,6 +145,7 @@ M.atto_image = M.atto_image || {
dialogue: null,
selection: null,
currentlyselected : {},
lastselectedimage : null,
init: function(params) {
var display_chooser = function(e, elementid) {
e.preventDefault();
@@ -183,7 +184,7 @@ M.atto_image = M.atto_image || {
// This includes cursor changes, we check if the button should be active or not, based
// on the text selection.
M.editor_atto.on('atto:selectionchanged', function(e) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes)) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes, false)) {
M.editor_atto.add_widget_highlight(e.elementid, 'image');
M.editor_atto.currentlyselected[e.elementid] = e.selectedNodes;
} else {
@@ -263,8 +264,13 @@ M.atto_image = M.atto_image || {
M.atto_image.dialogue.hide();
M.editor_atto.focus(elementid);
if (url !== '') {
M.editor_atto.set_selection(M.atto_image.selection);
if (this.lastselectedimage) {
M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage));
} else {
M.editor_atto.set_selection(M.atto_image.selection);
}
imagehtml = '<img src="' + Y.Escape.html(url) + '" alt="' + Y.Escape.html(alt) + '"';
if (width) {
@@ -310,14 +316,14 @@ M.atto_image = M.atto_image || {
images = M.editor_atto.currentlyselected[elementid],
i, image, width, height, style;
if (images !== null && images.size() > 0) {
if (images) {
images = images.filter('img');
}
if (images && images.size()) {
image = images.item(0);
for (i in images) {
if (images[i].test('img')) {
image = images[i];
break;
}
}
this.lastselectedimage = image;
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
+15 -9
View File
@@ -143,6 +143,7 @@ M.atto_image = M.atto_image || {
dialogue: null,
selection: null,
currentlyselected : {},
lastselectedimage : null,
init: function(params) {
var display_chooser = function(e, elementid) {
e.preventDefault();
@@ -181,7 +182,7 @@ M.atto_image = M.atto_image || {
// This includes cursor changes, we check if the button should be active or not, based
// on the text selection.
M.editor_atto.on('atto:selectionchanged', function(e) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes)) {
if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes, false)) {
M.editor_atto.add_widget_highlight(e.elementid, 'image');
M.editor_atto.currentlyselected[e.elementid] = e.selectedNodes;
} else {
@@ -261,8 +262,13 @@ M.atto_image = M.atto_image || {
M.atto_image.dialogue.hide();
M.editor_atto.focus(elementid);
if (url !== '') {
M.editor_atto.set_selection(M.atto_image.selection);
if (this.lastselectedimage) {
M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage));
} else {
M.editor_atto.set_selection(M.atto_image.selection);
}
imagehtml = '<img src="' + Y.Escape.html(url) + '" alt="' + Y.Escape.html(alt) + '"';
if (width) {
@@ -308,14 +314,14 @@ M.atto_image = M.atto_image || {
images = M.editor_atto.currentlyselected[elementid],
i, image, width, height, style;
if (images !== null && images.size() > 0) {
if (images) {
images = images.filter('img');
}
if (images && images.size()) {
image = images.item(0);
for (i in images) {
if (images[i].test('img')) {
image = images[i];
break;
}
}
this.lastselectedimage = image;
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<libraries>
<library>
<location>yui/src/rangy/js/*.*</location>
<name>Rangy</name>
<license>MIT</license>
<version>1.2.3</version>
<licenseversion></licenseversion>
</library>
</libraries>
@@ -84,7 +84,7 @@ M.editor_atto = M.editor_atto || {
'tfoot',
'ul',
'video'],
PLACEHOLDER_FONTNAME: 'yui-tmp',
ALL_NODES_SELECTOR: '[style],font[face]',
FONT_FAMILY: 'fontFamily',
@@ -140,13 +140,6 @@ M.editor_atto = M.editor_atto || {
*/
lastselection : null,
/**
* The number of selection ranges when the selection was last recorded.
* @param lastselectioncount
* @type integer
*/
lastselectioncount : 0,
/**
* Set to true after the first editor has been initialised.
*
@@ -569,22 +562,17 @@ M.editor_atto = M.editor_atto || {
* @return bool
*/
is_active : function(elementid) {
var selection = M.editor_atto.get_selection();
var range = rangy.createRange(),
editable = M.editor_atto.get_editable_node(elementid),
selection = rangy.getSelection();
if (selection.length && selection.pop) {
selection = selection.pop();
if (!selection.rangeCount) {
return false;
}
var node = null;
if (selection.parentElement) {
node = Y.one(selection.parentElement());
} else {
node = Y.one(selection.startContainer);
}
range.selectNode(editable.getDOMNode());
var editable = M.editor_atto.get_editable_node(elementid);
return node && editable.contains(node);
return range.intersectsRange(selection.getRangeAt(0));
},
/**
@@ -886,23 +874,12 @@ M.editor_atto = M.editor_atto || {
/**
* Create a cross browser selection object that represents a yui node.
* @param Node yui node for the selection
* @return range (browser dependent)
* @return rangy.Range[]
*/
get_selection_from_node: function(node) {
var range;
if (window.getSelection) {
range = document.createRange();
range.setStartBefore(node.getDOMNode());
range.setEndAfter(node.getDOMNode());
return [range];
} else if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText(node.getDOMNode());
return range;
}
return false;
var range = rangy.createRange();
range.selectNode(node.getDOMNode());
return [range];
},
/**
@@ -913,10 +890,7 @@ M.editor_atto = M.editor_atto || {
save_selection : function(event) {
var elementid = event.elementid;
if (elementid && this.is_active(elementid)) {
var sel = this.get_selection();
if (sel.length > 0) {
this.selections[elementid] = sel;
}
this.selections[elementid] = this.get_selection();
}
},
@@ -936,23 +910,10 @@ M.editor_atto = M.editor_atto || {
/**
* Get the selection object that can be passed back to set_selection.
* @return Range[]|TextRange[] All browsers except IE8 and less will get Range[], IE8 and less will get TextRange[]
* @return rangy.Range[]
*/
get_selection : function() {
if (window.getSelection) {
var sel = window.getSelection();
var ranges = [], i = 0;
for (i = 0; i < sel.rangeCount; i++) {
ranges.push(sel.getRangeAt(i));
}
return ranges;
} else if (document.selection) {
// IE < 9
if (document.selection.createRange) {
return [document.selection.createRange()];
}
}
return false;
return rangy.getSelection().getAllRanges();
},
/**
@@ -961,18 +922,7 @@ M.editor_atto = M.editor_atto || {
* @return boolean
*/
selection_contains_node : function(node) {
// TODO: This will break in IE11 . IE11 doesn't provide containsNode and document.selection has been removed.
var range, sel;
if (window.getSelection) {
sel = window.getSelection();
if (sel.containsNode) {
return sel.containsNode(node.getDOMNode(), true);
}
}
sel = document.selection.createRange();
range = sel.duplicate();
range.moveToElementText(node.getDOMNode());
return sel.inRange(range);
return rangy.getSelection().containsNode(node.getDOMNode(), true);
},
/**
@@ -982,124 +932,72 @@ M.editor_atto = M.editor_atto || {
* @param {String} elementid
* @param {String} selector
* @param {NodeList} selectednodes (Optional) - for performance we can pass this instead of looking it up.
* @param {Boolean} requireall (Optional) - used to specify that any "any" match is good enough. Defaults to true.
* @return {Boolean}
*/
selection_filter_matches : function(elementid, selector, selectednodes) {
selection_filter_matches : function(elementid, selector, selectednodes, requireall) {
if ((typeof requireall) === 'undefined') {
requireall = true;
}
if (!selectednodes) {
// Find this because it was not passed as a param.
selectednodes = M.editor_atto.get_selected_nodes(elementid);
}
var allmatch = selectednodes.size() > 0;
var allmatch = selectednodes.size() > 0, anymatch = false;
selectednodes.each(function(node){
// Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
if (requireall) {
// Check for at least one failure.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
}
} else {
// Check for at least one match.
if (!anymatch && node.ancestor(selector, true, '#' + elementid + "editable")) {
anymatch = true;
}
}
}, this);
return allmatch;
if (requireall) {
return allmatch;
} else {
return anymatch;
}
},
/**
* Get the deepest possible list of nodes in the current selection.
*
* Presently this method contains logic to work out what nodes are "selected" by the user.
* The user selection may take the form of:
* - text : selected text within a single node, such as double clicking a word.
* - A node : the user has clicked an image or media object that is selectable by clicking upon an element.
* - text + nodes: the user has dragged or used shift + arrows to select a body of content that may include
* both text and/or nodes.
* To work out the selected nodes we look at the start and end node. If they are the same then there is only one node
* containing selected content.
* If they don't match we get the common ancestor of the start and end nodes, and then iterate all child nodes recording
* all nodes that appear after the start node and before the end node.
*
* YUI deal with this in a totally different way to us, they have a method getSelected that gets the selected nodes by
* exec'ing a command to add fontname, and then find all nodes that have been affected. They remove the fontname property
* before returning the nodes.
*
* @param {String} elementid
* @return Y.NodeList
*/
get_selected_nodes : function(elementid) {
var nodes = [],
results = new Y.NodeList(),
points = [],
var results = new Y.NodeList(),
nodes,
selection,
range,
i,
sel,
start,
end,
parent,
values;
node,
i;
// The window.getSelection method is available from IE9 onwards and for all other browsers.
if (window.getSelection) {
sel = window.getSelection();
for (i = 0; i < sel.rangeCount; i++) {
range = sel.getRangeAt(i);
start = new Y.Node(range.startContainer);
end = new Y.Node(range.endContainer);
if (start.compareTo(end)) {
// The start and end node are the same node. Only one node is selected. Easy!
nodes.push(end);
} else {
// The start and end node are not the same, we have 2..n nodes selected, we'll need to work them out.
// For the time being record them as a pair of points along with the common ancestor.
// First up we need to check the order of the nodes, we need them in the order they appear in the DOM
// however the selection may have been rtl or ltr and in the case of rtl we need to switch the order.
values = this._fix_selected_nodes_from_range(
start,
range.startOffset,
end,
range.endOffset
);
// Ok start and end are in order. Yay!
start = values[0];
end = values[2];
// We calculate the common ancestor here as we should be able to get it from the range object.
if (range.commonAncestorContainer) {
parent = range.commonAncestorContainer;
} else if (range.parentElement) {
parent = range.parentElement();
} else {
parent = this._get_common_parent_from_nodes(start, end);
}
points.push([
start,
end,
parent
]);
}
}
} else if (document.selection && document.selection.createRange) {
// The document.selection.createRange is available in IE only, versions 5.5 through 10 (not IE11).
// However s window.getSelection exists in IE9+ this code will only execute in IE 5.5 -> IE 8.
// http://msdn.microsoft.com/en-us/library/ie/ms535869(v=vs.85).aspx
range = document.selection.createRange();
// Create range will return either TextRange object from the current text selection, or a controlRange
// collection from the control selection.
if (document.selection.type === "Control") {
// Its a control collection, well this is easy, its an array of Nodes. All the work has been done for us.
for (i = 0; i < range.length; i++) {
nodes.push(new Y.Node(range(i)));
}
} else {
// Its a TextRange, in which case its a text element (nodeType 3) and its parent will be the node containing the
// selection.
nodes.push(new Y.Node(range.parentElement()));
}
selection = rangy.getSelection();
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
// Ok we have two array as this point.
// - Nodes is an array containing nodes that are currently highlighted by selection.
// - Points is an array containing start, end, and parent nodes.
// For each item in the points array we need to find all nodes containing selected content.
for (i in points) {
nodes = nodes.concat(this._get_all_nodes_between.apply(this, points[i]));
if (range.collapsed) {
range.selectNode(range.commonAncestorContainer);
}
// Finally for each node we've determined is highlighted we need to check it belongs to the current editable area.
for (i in nodes) {
if (results.indexOf(nodes[i]) === -1 && nodes[i].ancestor('#' + elementid + "editable")) {
results.push(nodes[i]);
nodes = range.getNodes();
for (i = 0; i < nodes.length; i++) {
node = Y.one(nodes[i]);
if (node.ancestor('#' + elementid + "editable")) {
results.push(node);
}
}
return results;
@@ -1116,47 +1014,25 @@ M.editor_atto = M.editor_atto || {
* @returns {Boolean}
*/
_has_selection_changed : function(e) {
var sel,
i,
var selection = rangy.getSelection(),
range,
bits = [],
selectedstr = '',
selectioncount = 0;
if (window.getSelection) {
// Hoorah - working with the alternative is horrid..
sel = window.getSelection();
selectioncount = sel.rangeCount;
for (i = 0; i < selectioncount; i++) {
range = sel.getRangeAt(i);
bits.push(range.startOffset);
bits.push(range.endOffset);
bits.push(this._get_node_identifier(range.startContainer));
bits.push(this._get_node_identifier(range.endContainer));
bits.push('&');
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range !== null) {
if (document.selection.type === "Control") {
// Is a controlRange: http://msdn.microsoft.com/en-us/library/ie/hh826021(v=vs.85).aspx
selectioncount = range.length;
for (i = 0; i < selectioncount; i++) {
bits.push(this._get_node_identifier(range(i)));
}
} else {
// Is a TextRange: http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
selectioncount = 1;
bits.push(this._get_node_identifier(range.parentElement()));
}
changed = false;
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
if (this.lastselection) {
if (!this.lastselection.equals(range)) {
changed = true;
return this._fire_selectionchanged(e);
}
}
selectedstr = bits.join('|');
if (this.lastselection !== selectedstr) {
this.lastselection = selectedstr;
this.lastselectioncount = selectioncount;
return this._fire_selectionchanged(e);
}
return false;
this.lastselection = range;
return changed;
},
/**
@@ -1182,230 +1058,25 @@ M.editor_atto = M.editor_atto || {
});
},
/**
* Returns the best identifier for the given node that can be found. May not be unique.
*
* @method _get_node_identifier
* @private
* @param {Node} node
* @returns {string}
*/
_get_node_identifier : function(node) {
var uid = (node.nodeType === 3) ? node.length : node.id;
return uid || node.className || node.nodeName;
},
/**
* Fixes the selection range, normalising it, ordering the start and end by dom position, and correcting for noteType and offset
*
* Note about offsets:
* If the container is a Node of type Text, Comment, or CDATASection, then the offset is the number of characters from the
* start of the container to the boundary point of the Range.
* For other Node types, the offset is the number of child nodes between the start of the container and the boundary
* point of the Range.
*
* @method _fix_selected_nodes_from_range
* @private
* @param {Node} a
* @param {integer} aoffset
* @param {Node} b
* @param {integer} boffset
* @returns {[Node, integer, Node, integer]}
*/
_fix_selected_nodes_from_range : function(a, aoffset, b, boffset) {
var first,
firstoffset,
second,
secondoffset,
searchfunc,
acheckend = true,
bcheckend = true,
checkend = true;
if (a.get('nodeType') === 1) {
a = a.get('childNodes').item(aoffset);
acheckend = false;
}
if (b.get('nodeType') === 1 && boffset > 0) {
b = b.get('childNodes').item(boffset - 1);
bcheckend = false;
}
if (a._yuid === b._yuid) {
if (aoffset < boffset) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
} else {
if (a.contains(b)) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else if (b.contains(a)) {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
} else {
searchfunc = function find_first(nodes, x, y) {
var i, result;
for (i in nodes) {
if (nodes[i] === x.getDOMNode()) {
return x;
} else if (nodes[i] === y.getDOMNode()) {
return y;
} else if (nodes[i].nodeType === 1) {
result = find_first(nodes[i].childNodes, x, y);
if (result !== false) {
return result;
}
}
}
return false;
};
first = searchfunc(this._get_common_parent_from_nodes(a, b).get('childNodes'), a, b) || a;
if (first === a) {
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
}
if (checkend && second.get('nodeType') === 3 && secondoffset === 0 && second.previous('*', true)) {
second = second.previous();
}
}
return [first, firstoffset, second, secondoffset];
},
/**
* Returns an array containing all of the nodes in the Dom between the two reference nodes.
*
* @method _get_all_nodes_between
* @private
* @param {Node} a
* @param {Node} b
* @param {Node} parent The common parent of a and b
* @returns {Node[]}
*/
_get_all_nodes_between : function(a, b, parent) {
var startfound = false,
endfound = false,
nodes = [],
parenthasselectedtext = false;
if (a === b || a.contains(b)) {
return [a];
}
if (b.contains(a)) {
return [b];
}
if (!parent) {
return [a, b];
}
nodes.push(a);
parent.get('childNodes').each(function(node) {
if (endfound) {
return;
}
if (!startfound && node === a) {
startfound = true;
} else if (startfound && !endfound) {
if (node === b) {
endfound = true;
} else {
if (node.get('nodeType') === 3) {
parenthasselectedtext = true;
} else {
nodes.push(node);
nodes = nodes.compact(node.all('*'));
}
}
}
});
nodes.push(b);
if (parenthasselectedtext) {
nodes.unshift(parent);
}
return nodes;
},
/**
* Returns the common parent of the two given nodes.
*
* @method _get_common_parent_from_nodes
* @private
* @param {Node} a
* @param {Node} b
* @returns {Node}
*/
_get_common_parent_from_nodes : function(a, b) {
var aparents = a.ancestors('*', false, '.' + CSS.CONTENT),
bparents = b.ancestors('*', false, '.' + CSS.CONTENT),
node;
while ((node = aparents.pop())) {
if (bparents.indexOf(node) !== -1) {
return node;
}
}
return a.ancestor('.' + CSS.CONTENT);
},
/**
* Get the dom node representing the common anscestor of the selection nodes.
* @return DOMNode or false
*/
get_selection_parent_node : function() {
var selection = M.editor_atto.get_selection();
if (selection.length) {
selection = selection.pop();
var selection = rangy.getSelection();
if (selection.rangeCount) {
return selection.getRangeAt(0).commonAncestorContainer;
}
if (selection.commonAncestorContainer) {
return selection.commonAncestorContainer;
} else if (selection.parentElement) {
return selection.parentElement();
}
// No selection
return false;
},
/**
* Set the current selection. Used to restore a selection.
* @param rangy.Range[] ranges - List of ranges in selection.
*/
set_selection : function(selection) {
var sel, i;
if (window.getSelection) {
sel = window.getSelection();
sel.removeAllRanges();
for (i = 0; i < selection.length; i++) {
sel.addRange(selection[i]);
}
} else if (document.selection) {
// IE < 9
if (selection.select) {
selection.select();
}
}
set_selection : function(ranges) {
var selection = rangy.getSelection();
selection.setRanges(ranges);
},
/**
@@ -1552,19 +1223,20 @@ M.editor_atto = M.editor_atto || {
/**
* Inserts the given HTML into the editable content at the currently focused point.
* @param {String} html
* @param String html
*/
insert_html_at_focus_point: function(html) {
// We check document.selection here as the insertHTML exec command wan't available in IE until version 11.
// In IE document.selection was removed at from version 11. Making it an easy affordable check.
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
if (range.pasteHTML) {
range.pasteHTML(html);
}
} else {
// All other browsers are great!
document.execCommand('insertHTML', false, html);
var selection = rangy.getSelection(),
range,
node = Y.Node.create(html);
if (selection.rangeCount) {
range = selection.getRangeAt(0);
}
if (range) {
range.deleteContents();
range.insertNode(node.getDOMNode());
}
},
@@ -1775,6 +1447,7 @@ Y.NodeList.importMethod(Y.Node.prototype, "cleanHTML");
"event-simulate",
"event-custom",
"yui-throttle",
"moodle-core-notification-dialogue"
"moodle-core-notification-dialogue",
"moodle-editor_atto-rangy"
]
});
File diff suppressed because one or more lines are too long
@@ -84,7 +84,7 @@ M.editor_atto = M.editor_atto || {
'tfoot',
'ul',
'video'],
PLACEHOLDER_FONTNAME: 'yui-tmp',
ALL_NODES_SELECTOR: '[style],font[face]',
FONT_FAMILY: 'fontFamily',
@@ -140,13 +140,6 @@ M.editor_atto = M.editor_atto || {
*/
lastselection : null,
/**
* The number of selection ranges when the selection was last recorded.
* @param lastselectioncount
* @type integer
*/
lastselectioncount : 0,
/**
* Set to true after the first editor has been initialised.
*
@@ -569,22 +562,17 @@ M.editor_atto = M.editor_atto || {
* @return bool
*/
is_active : function(elementid) {
var selection = M.editor_atto.get_selection();
var range = rangy.createRange(),
editable = M.editor_atto.get_editable_node(elementid),
selection = rangy.getSelection();
if (selection.length && selection.pop) {
selection = selection.pop();
if (!selection.rangeCount) {
return false;
}
var node = null;
if (selection.parentElement) {
node = Y.one(selection.parentElement());
} else {
node = Y.one(selection.startContainer);
}
range.selectNode(editable.getDOMNode());
var editable = M.editor_atto.get_editable_node(elementid);
return node && editable.contains(node);
return range.intersectsRange(selection.getRangeAt(0));
},
/**
@@ -886,23 +874,12 @@ M.editor_atto = M.editor_atto || {
/**
* Create a cross browser selection object that represents a yui node.
* @param Node yui node for the selection
* @return range (browser dependent)
* @return rangy.Range[]
*/
get_selection_from_node: function(node) {
var range;
if (window.getSelection) {
range = document.createRange();
range.setStartBefore(node.getDOMNode());
range.setEndAfter(node.getDOMNode());
return [range];
} else if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText(node.getDOMNode());
return range;
}
return false;
var range = rangy.createRange();
range.selectNode(node.getDOMNode());
return [range];
},
/**
@@ -913,10 +890,7 @@ M.editor_atto = M.editor_atto || {
save_selection : function(event) {
var elementid = event.elementid;
if (elementid && this.is_active(elementid)) {
var sel = this.get_selection();
if (sel.length > 0) {
this.selections[elementid] = sel;
}
this.selections[elementid] = this.get_selection();
}
},
@@ -936,23 +910,10 @@ M.editor_atto = M.editor_atto || {
/**
* Get the selection object that can be passed back to set_selection.
* @return Range[]|TextRange[] All browsers except IE8 and less will get Range[], IE8 and less will get TextRange[]
* @return rangy.Range[]
*/
get_selection : function() {
if (window.getSelection) {
var sel = window.getSelection();
var ranges = [], i = 0;
for (i = 0; i < sel.rangeCount; i++) {
ranges.push(sel.getRangeAt(i));
}
return ranges;
} else if (document.selection) {
// IE < 9
if (document.selection.createRange) {
return [document.selection.createRange()];
}
}
return false;
return rangy.getSelection().getAllRanges();
},
/**
@@ -961,18 +922,7 @@ M.editor_atto = M.editor_atto || {
* @return boolean
*/
selection_contains_node : function(node) {
// TODO: This will break in IE11 . IE11 doesn't provide containsNode and document.selection has been removed.
var range, sel;
if (window.getSelection) {
sel = window.getSelection();
if (sel.containsNode) {
return sel.containsNode(node.getDOMNode(), true);
}
}
sel = document.selection.createRange();
range = sel.duplicate();
range.moveToElementText(node.getDOMNode());
return sel.inRange(range);
return rangy.getSelection().containsNode(node.getDOMNode(), true);
},
/**
@@ -982,124 +932,72 @@ M.editor_atto = M.editor_atto || {
* @param {String} elementid
* @param {String} selector
* @param {NodeList} selectednodes (Optional) - for performance we can pass this instead of looking it up.
* @param {Boolean} requireall (Optional) - used to specify that any "any" match is good enough. Defaults to true.
* @return {Boolean}
*/
selection_filter_matches : function(elementid, selector, selectednodes) {
selection_filter_matches : function(elementid, selector, selectednodes, requireall) {
if ((typeof requireall) === 'undefined') {
requireall = true;
}
if (!selectednodes) {
// Find this because it was not passed as a param.
selectednodes = M.editor_atto.get_selected_nodes(elementid);
}
var allmatch = selectednodes.size() > 0;
var allmatch = selectednodes.size() > 0, anymatch = false;
selectednodes.each(function(node){
// Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
if (requireall) {
// Check for at least one failure.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
}
} else {
// Check for at least one match.
if (!anymatch && node.ancestor(selector, true, '#' + elementid + "editable")) {
anymatch = true;
}
}
}, this);
return allmatch;
if (requireall) {
return allmatch;
} else {
return anymatch;
}
},
/**
* Get the deepest possible list of nodes in the current selection.
*
* Presently this method contains logic to work out what nodes are "selected" by the user.
* The user selection may take the form of:
* - text : selected text within a single node, such as double clicking a word.
* - A node : the user has clicked an image or media object that is selectable by clicking upon an element.
* - text + nodes: the user has dragged or used shift + arrows to select a body of content that may include
* both text and/or nodes.
* To work out the selected nodes we look at the start and end node. If they are the same then there is only one node
* containing selected content.
* If they don't match we get the common ancestor of the start and end nodes, and then iterate all child nodes recording
* all nodes that appear after the start node and before the end node.
*
* YUI deal with this in a totally different way to us, they have a method getSelected that gets the selected nodes by
* exec'ing a command to add fontname, and then find all nodes that have been affected. They remove the fontname property
* before returning the nodes.
*
* @param {String} elementid
* @return Y.NodeList
*/
get_selected_nodes : function(elementid) {
var nodes = [],
results = new Y.NodeList(),
points = [],
var results = new Y.NodeList(),
nodes,
selection,
range,
i,
sel,
start,
end,
parent,
values;
node,
i;
// The window.getSelection method is available from IE9 onwards and for all other browsers.
if (window.getSelection) {
sel = window.getSelection();
for (i = 0; i < sel.rangeCount; i++) {
range = sel.getRangeAt(i);
start = new Y.Node(range.startContainer);
end = new Y.Node(range.endContainer);
if (start.compareTo(end)) {
// The start and end node are the same node. Only one node is selected. Easy!
nodes.push(end);
} else {
// The start and end node are not the same, we have 2..n nodes selected, we'll need to work them out.
// For the time being record them as a pair of points along with the common ancestor.
// First up we need to check the order of the nodes, we need them in the order they appear in the DOM
// however the selection may have been rtl or ltr and in the case of rtl we need to switch the order.
values = this._fix_selected_nodes_from_range(
start,
range.startOffset,
end,
range.endOffset
);
// Ok start and end are in order. Yay!
start = values[0];
end = values[2];
// We calculate the common ancestor here as we should be able to get it from the range object.
if (range.commonAncestorContainer) {
parent = range.commonAncestorContainer;
} else if (range.parentElement) {
parent = range.parentElement();
} else {
parent = this._get_common_parent_from_nodes(start, end);
}
points.push([
start,
end,
parent
]);
}
}
} else if (document.selection && document.selection.createRange) {
// The document.selection.createRange is available in IE only, versions 5.5 through 10 (not IE11).
// However s window.getSelection exists in IE9+ this code will only execute in IE 5.5 -> IE 8.
// http://msdn.microsoft.com/en-us/library/ie/ms535869(v=vs.85).aspx
range = document.selection.createRange();
// Create range will return either TextRange object from the current text selection, or a controlRange
// collection from the control selection.
if (document.selection.type === "Control") {
// Its a control collection, well this is easy, its an array of Nodes. All the work has been done for us.
for (i = 0; i < range.length; i++) {
nodes.push(new Y.Node(range(i)));
}
} else {
// Its a TextRange, in which case its a text element (nodeType 3) and its parent will be the node containing the
// selection.
nodes.push(new Y.Node(range.parentElement()));
}
selection = rangy.getSelection();
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
// Ok we have two array as this point.
// - Nodes is an array containing nodes that are currently highlighted by selection.
// - Points is an array containing start, end, and parent nodes.
// For each item in the points array we need to find all nodes containing selected content.
for (i in points) {
nodes = nodes.concat(this._get_all_nodes_between.apply(this, points[i]));
if (range.collapsed) {
range.selectNode(range.commonAncestorContainer);
}
// Finally for each node we've determined is highlighted we need to check it belongs to the current editable area.
for (i in nodes) {
if (results.indexOf(nodes[i]) === -1 && nodes[i].ancestor('#' + elementid + "editable")) {
results.push(nodes[i]);
nodes = range.getNodes();
for (i = 0; i < nodes.length; i++) {
node = Y.one(nodes[i]);
if (node.ancestor('#' + elementid + "editable")) {
results.push(node);
}
}
return results;
@@ -1116,47 +1014,25 @@ M.editor_atto = M.editor_atto || {
* @returns {Boolean}
*/
_has_selection_changed : function(e) {
var sel,
i,
var selection = rangy.getSelection(),
range,
bits = [],
selectedstr = '',
selectioncount = 0;
if (window.getSelection) {
// Hoorah - working with the alternative is horrid..
sel = window.getSelection();
selectioncount = sel.rangeCount;
for (i = 0; i < selectioncount; i++) {
range = sel.getRangeAt(i);
bits.push(range.startOffset);
bits.push(range.endOffset);
bits.push(this._get_node_identifier(range.startContainer));
bits.push(this._get_node_identifier(range.endContainer));
bits.push('&');
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range !== null) {
if (document.selection.type === "Control") {
// Is a controlRange: http://msdn.microsoft.com/en-us/library/ie/hh826021(v=vs.85).aspx
selectioncount = range.length;
for (i = 0; i < selectioncount; i++) {
bits.push(this._get_node_identifier(range(i)));
}
} else {
// Is a TextRange: http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
selectioncount = 1;
bits.push(this._get_node_identifier(range.parentElement()));
}
changed = false;
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
if (this.lastselection) {
if (!this.lastselection.equals(range)) {
changed = true;
return this._fire_selectionchanged(e);
}
}
selectedstr = bits.join('|');
if (this.lastselection !== selectedstr) {
this.lastselection = selectedstr;
this.lastselectioncount = selectioncount;
return this._fire_selectionchanged(e);
}
return false;
this.lastselection = range;
return changed;
},
/**
@@ -1182,230 +1058,25 @@ M.editor_atto = M.editor_atto || {
});
},
/**
* Returns the best identifier for the given node that can be found. May not be unique.
*
* @method _get_node_identifier
* @private
* @param {Node} node
* @returns {string}
*/
_get_node_identifier : function(node) {
var uid = (node.nodeType === 3) ? node.length : node.id;
return uid || node.className || node.nodeName;
},
/**
* Fixes the selection range, normalising it, ordering the start and end by dom position, and correcting for noteType and offset
*
* Note about offsets:
* If the container is a Node of type Text, Comment, or CDATASection, then the offset is the number of characters from the
* start of the container to the boundary point of the Range.
* For other Node types, the offset is the number of child nodes between the start of the container and the boundary
* point of the Range.
*
* @method _fix_selected_nodes_from_range
* @private
* @param {Node} a
* @param {integer} aoffset
* @param {Node} b
* @param {integer} boffset
* @returns {[Node, integer, Node, integer]}
*/
_fix_selected_nodes_from_range : function(a, aoffset, b, boffset) {
var first,
firstoffset,
second,
secondoffset,
searchfunc,
acheckend = true,
bcheckend = true,
checkend = true;
if (a.get('nodeType') === 1) {
a = a.get('childNodes').item(aoffset);
acheckend = false;
}
if (b.get('nodeType') === 1 && boffset > 0) {
b = b.get('childNodes').item(boffset - 1);
bcheckend = false;
}
if (a._yuid === b._yuid) {
if (aoffset < boffset) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
} else {
if (a.contains(b)) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else if (b.contains(a)) {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
} else {
searchfunc = function find_first(nodes, x, y) {
var i, result;
for (i in nodes) {
if (nodes[i] === x.getDOMNode()) {
return x;
} else if (nodes[i] === y.getDOMNode()) {
return y;
} else if (nodes[i].nodeType === 1) {
result = find_first(nodes[i].childNodes, x, y);
if (result !== false) {
return result;
}
}
}
return false;
};
first = searchfunc(this._get_common_parent_from_nodes(a, b).get('childNodes'), a, b) || a;
if (first === a) {
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
}
if (checkend && second.get('nodeType') === 3 && secondoffset === 0 && second.previous('*', true)) {
second = second.previous();
}
}
return [first, firstoffset, second, secondoffset];
},
/**
* Returns an array containing all of the nodes in the Dom between the two reference nodes.
*
* @method _get_all_nodes_between
* @private
* @param {Node} a
* @param {Node} b
* @param {Node} parent The common parent of a and b
* @returns {Node[]}
*/
_get_all_nodes_between : function(a, b, parent) {
var startfound = false,
endfound = false,
nodes = [],
parenthasselectedtext = false;
if (a === b || a.contains(b)) {
return [a];
}
if (b.contains(a)) {
return [b];
}
if (!parent) {
return [a, b];
}
nodes.push(a);
parent.get('childNodes').each(function(node) {
if (endfound) {
return;
}
if (!startfound && node === a) {
startfound = true;
} else if (startfound && !endfound) {
if (node === b) {
endfound = true;
} else {
if (node.get('nodeType') === 3) {
parenthasselectedtext = true;
} else {
nodes.push(node);
nodes = nodes.compact(node.all('*'));
}
}
}
});
nodes.push(b);
if (parenthasselectedtext) {
nodes.unshift(parent);
}
return nodes;
},
/**
* Returns the common parent of the two given nodes.
*
* @method _get_common_parent_from_nodes
* @private
* @param {Node} a
* @param {Node} b
* @returns {Node}
*/
_get_common_parent_from_nodes : function(a, b) {
var aparents = a.ancestors('*', false, '.' + CSS.CONTENT),
bparents = b.ancestors('*', false, '.' + CSS.CONTENT),
node;
while ((node = aparents.pop())) {
if (bparents.indexOf(node) !== -1) {
return node;
}
}
return a.ancestor('.' + CSS.CONTENT);
},
/**
* Get the dom node representing the common anscestor of the selection nodes.
* @return DOMNode or false
*/
get_selection_parent_node : function() {
var selection = M.editor_atto.get_selection();
if (selection.length) {
selection = selection.pop();
var selection = rangy.getSelection();
if (selection.rangeCount) {
return selection.getRangeAt(0).commonAncestorContainer;
}
if (selection.commonAncestorContainer) {
return selection.commonAncestorContainer;
} else if (selection.parentElement) {
return selection.parentElement();
}
// No selection
return false;
},
/**
* Set the current selection. Used to restore a selection.
* @param rangy.Range[] ranges - List of ranges in selection.
*/
set_selection : function(selection) {
var sel, i;
if (window.getSelection) {
sel = window.getSelection();
sel.removeAllRanges();
for (i = 0; i < selection.length; i++) {
sel.addRange(selection[i]);
}
} else if (document.selection) {
// IE < 9
if (selection.select) {
selection.select();
}
}
set_selection : function(ranges) {
var selection = rangy.getSelection();
selection.setRanges(ranges);
},
/**
@@ -1552,19 +1223,20 @@ M.editor_atto = M.editor_atto || {
/**
* Inserts the given HTML into the editable content at the currently focused point.
* @param {String} html
* @param String html
*/
insert_html_at_focus_point: function(html) {
// We check document.selection here as the insertHTML exec command wan't available in IE until version 11.
// In IE document.selection was removed at from version 11. Making it an easy affordable check.
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
if (range.pasteHTML) {
range.pasteHTML(html);
}
} else {
// All other browsers are great!
document.execCommand('insertHTML', false, html);
var selection = rangy.getSelection(),
range,
node = Y.Node.create(html);
if (selection.rangeCount) {
range = selection.getRangeAt(0);
}
if (range) {
range.deleteContents();
range.insertNode(node.getDOMNode());
}
},
@@ -1775,6 +1447,7 @@ Y.NodeList.importMethod(Y.Node.prototype, "cleanHTML");
"event-simulate",
"event-custom",
"yui-throttle",
"moodle-core-notification-dialogue"
"moodle-core-notification-dialogue",
"moodle-editor_atto-rangy"
]
});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+92 -420
View File
@@ -82,7 +82,7 @@ M.editor_atto = M.editor_atto || {
'tfoot',
'ul',
'video'],
PLACEHOLDER_FONTNAME: 'yui-tmp',
ALL_NODES_SELECTOR: '[style],font[face]',
FONT_FAMILY: 'fontFamily',
@@ -138,13 +138,6 @@ M.editor_atto = M.editor_atto || {
*/
lastselection : null,
/**
* The number of selection ranges when the selection was last recorded.
* @param lastselectioncount
* @type integer
*/
lastselectioncount : 0,
/**
* Set to true after the first editor has been initialised.
*
@@ -567,22 +560,17 @@ M.editor_atto = M.editor_atto || {
* @return bool
*/
is_active : function(elementid) {
var selection = M.editor_atto.get_selection();
var range = rangy.createRange(),
editable = M.editor_atto.get_editable_node(elementid),
selection = rangy.getSelection();
if (selection.length && selection.pop) {
selection = selection.pop();
if (!selection.rangeCount) {
return false;
}
var node = null;
if (selection.parentElement) {
node = Y.one(selection.parentElement());
} else {
node = Y.one(selection.startContainer);
}
range.selectNode(editable.getDOMNode());
var editable = M.editor_atto.get_editable_node(elementid);
return node && editable.contains(node);
return range.intersectsRange(selection.getRangeAt(0));
},
/**
@@ -884,23 +872,12 @@ M.editor_atto = M.editor_atto || {
/**
* Create a cross browser selection object that represents a yui node.
* @param Node yui node for the selection
* @return range (browser dependent)
* @return rangy.Range[]
*/
get_selection_from_node: function(node) {
var range;
if (window.getSelection) {
range = document.createRange();
range.setStartBefore(node.getDOMNode());
range.setEndAfter(node.getDOMNode());
return [range];
} else if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText(node.getDOMNode());
return range;
}
return false;
var range = rangy.createRange();
range.selectNode(node.getDOMNode());
return [range];
},
/**
@@ -911,10 +888,7 @@ M.editor_atto = M.editor_atto || {
save_selection : function(event) {
var elementid = event.elementid;
if (elementid && this.is_active(elementid)) {
var sel = this.get_selection();
if (sel.length > 0) {
this.selections[elementid] = sel;
}
this.selections[elementid] = this.get_selection();
}
},
@@ -934,23 +908,10 @@ M.editor_atto = M.editor_atto || {
/**
* Get the selection object that can be passed back to set_selection.
* @return Range[]|TextRange[] All browsers except IE8 and less will get Range[], IE8 and less will get TextRange[]
* @return rangy.Range[]
*/
get_selection : function() {
if (window.getSelection) {
var sel = window.getSelection();
var ranges = [], i = 0;
for (i = 0; i < sel.rangeCount; i++) {
ranges.push(sel.getRangeAt(i));
}
return ranges;
} else if (document.selection) {
// IE < 9
if (document.selection.createRange) {
return [document.selection.createRange()];
}
}
return false;
return rangy.getSelection().getAllRanges();
},
/**
@@ -959,18 +920,7 @@ M.editor_atto = M.editor_atto || {
* @return boolean
*/
selection_contains_node : function(node) {
// TODO: This will break in IE11 . IE11 doesn't provide containsNode and document.selection has been removed.
var range, sel;
if (window.getSelection) {
sel = window.getSelection();
if (sel.containsNode) {
return sel.containsNode(node.getDOMNode(), true);
}
}
sel = document.selection.createRange();
range = sel.duplicate();
range.moveToElementText(node.getDOMNode());
return sel.inRange(range);
return rangy.getSelection().containsNode(node.getDOMNode(), true);
},
/**
@@ -980,124 +930,72 @@ M.editor_atto = M.editor_atto || {
* @param {String} elementid
* @param {String} selector
* @param {NodeList} selectednodes (Optional) - for performance we can pass this instead of looking it up.
* @param {Boolean} requireall (Optional) - used to specify that any "any" match is good enough. Defaults to true.
* @return {Boolean}
*/
selection_filter_matches : function(elementid, selector, selectednodes) {
selection_filter_matches : function(elementid, selector, selectednodes, requireall) {
if ((typeof requireall) === 'undefined') {
requireall = true;
}
if (!selectednodes) {
// Find this because it was not passed as a param.
selectednodes = M.editor_atto.get_selected_nodes(elementid);
}
var allmatch = selectednodes.size() > 0;
var allmatch = selectednodes.size() > 0, anymatch = false;
selectednodes.each(function(node){
// Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
if (requireall) {
// Check for at least one failure.
if (!allmatch || !node.ancestor(selector, true, '#' + elementid + "editable")) {
allmatch = false;
}
} else {
// Check for at least one match.
if (!anymatch && node.ancestor(selector, true, '#' + elementid + "editable")) {
anymatch = true;
}
}
}, this);
return allmatch;
if (requireall) {
return allmatch;
} else {
return anymatch;
}
},
/**
* Get the deepest possible list of nodes in the current selection.
*
* Presently this method contains logic to work out what nodes are "selected" by the user.
* The user selection may take the form of:
* - text : selected text within a single node, such as double clicking a word.
* - A node : the user has clicked an image or media object that is selectable by clicking upon an element.
* - text + nodes: the user has dragged or used shift + arrows to select a body of content that may include
* both text and/or nodes.
* To work out the selected nodes we look at the start and end node. If they are the same then there is only one node
* containing selected content.
* If they don't match we get the common ancestor of the start and end nodes, and then iterate all child nodes recording
* all nodes that appear after the start node and before the end node.
*
* YUI deal with this in a totally different way to us, they have a method getSelected that gets the selected nodes by
* exec'ing a command to add fontname, and then find all nodes that have been affected. They remove the fontname property
* before returning the nodes.
*
* @param {String} elementid
* @return Y.NodeList
*/
get_selected_nodes : function(elementid) {
var nodes = [],
results = new Y.NodeList(),
points = [],
var results = new Y.NodeList(),
nodes,
selection,
range,
i,
sel,
start,
end,
parent,
values;
node,
i;
// The window.getSelection method is available from IE9 onwards and for all other browsers.
if (window.getSelection) {
sel = window.getSelection();
for (i = 0; i < sel.rangeCount; i++) {
range = sel.getRangeAt(i);
start = new Y.Node(range.startContainer);
end = new Y.Node(range.endContainer);
if (start.compareTo(end)) {
// The start and end node are the same node. Only one node is selected. Easy!
nodes.push(end);
} else {
// The start and end node are not the same, we have 2..n nodes selected, we'll need to work them out.
// For the time being record them as a pair of points along with the common ancestor.
// First up we need to check the order of the nodes, we need them in the order they appear in the DOM
// however the selection may have been rtl or ltr and in the case of rtl we need to switch the order.
values = this._fix_selected_nodes_from_range(
start,
range.startOffset,
end,
range.endOffset
);
// Ok start and end are in order. Yay!
start = values[0];
end = values[2];
// We calculate the common ancestor here as we should be able to get it from the range object.
if (range.commonAncestorContainer) {
parent = range.commonAncestorContainer;
} else if (range.parentElement) {
parent = range.parentElement();
} else {
parent = this._get_common_parent_from_nodes(start, end);
}
points.push([
start,
end,
parent
]);
}
}
} else if (document.selection && document.selection.createRange) {
// The document.selection.createRange is available in IE only, versions 5.5 through 10 (not IE11).
// However s window.getSelection exists in IE9+ this code will only execute in IE 5.5 -> IE 8.
// http://msdn.microsoft.com/en-us/library/ie/ms535869(v=vs.85).aspx
range = document.selection.createRange();
// Create range will return either TextRange object from the current text selection, or a controlRange
// collection from the control selection.
if (document.selection.type === "Control") {
// Its a control collection, well this is easy, its an array of Nodes. All the work has been done for us.
for (i = 0; i < range.length; i++) {
nodes.push(new Y.Node(range(i)));
}
} else {
// Its a TextRange, in which case its a text element (nodeType 3) and its parent will be the node containing the
// selection.
nodes.push(new Y.Node(range.parentElement()));
}
selection = rangy.getSelection();
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
// Ok we have two array as this point.
// - Nodes is an array containing nodes that are currently highlighted by selection.
// - Points is an array containing start, end, and parent nodes.
// For each item in the points array we need to find all nodes containing selected content.
for (i in points) {
nodes = nodes.concat(this._get_all_nodes_between.apply(this, points[i]));
if (range.collapsed) {
range.selectNode(range.commonAncestorContainer);
}
// Finally for each node we've determined is highlighted we need to check it belongs to the current editable area.
for (i in nodes) {
if (results.indexOf(nodes[i]) === -1 && nodes[i].ancestor('#' + elementid + "editable")) {
results.push(nodes[i]);
nodes = range.getNodes();
for (i = 0; i < nodes.length; i++) {
node = Y.one(nodes[i]);
if (node.ancestor('#' + elementid + "editable")) {
results.push(node);
}
}
return results;
@@ -1114,47 +1012,25 @@ M.editor_atto = M.editor_atto || {
* @returns {Boolean}
*/
_has_selection_changed : function(e) {
var sel,
i,
var selection = rangy.getSelection(),
range,
bits = [],
selectedstr = '',
selectioncount = 0;
if (window.getSelection) {
// Hoorah - working with the alternative is horrid..
sel = window.getSelection();
selectioncount = sel.rangeCount;
for (i = 0; i < selectioncount; i++) {
range = sel.getRangeAt(i);
bits.push(range.startOffset);
bits.push(range.endOffset);
bits.push(this._get_node_identifier(range.startContainer));
bits.push(this._get_node_identifier(range.endContainer));
bits.push('&');
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range !== null) {
if (document.selection.type === "Control") {
// Is a controlRange: http://msdn.microsoft.com/en-us/library/ie/hh826021(v=vs.85).aspx
selectioncount = range.length;
for (i = 0; i < selectioncount; i++) {
bits.push(this._get_node_identifier(range(i)));
}
} else {
// Is a TextRange: http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
selectioncount = 1;
bits.push(this._get_node_identifier(range.parentElement()));
}
changed = false;
if (selection.rangeCount) {
range = selection.getRangeAt(0);
} else {
// Empty range.
range = rangy.createRange();
}
if (this.lastselection) {
if (!this.lastselection.equals(range)) {
changed = true;
return this._fire_selectionchanged(e);
}
}
selectedstr = bits.join('|');
if (this.lastselection !== selectedstr) {
this.lastselection = selectedstr;
this.lastselectioncount = selectioncount;
return this._fire_selectionchanged(e);
}
return false;
this.lastselection = range;
return changed;
},
/**
@@ -1180,230 +1056,25 @@ M.editor_atto = M.editor_atto || {
});
},
/**
* Returns the best identifier for the given node that can be found. May not be unique.
*
* @method _get_node_identifier
* @private
* @param {Node} node
* @returns {string}
*/
_get_node_identifier : function(node) {
var uid = (node.nodeType === 3) ? node.length : node.id;
return uid || node.className || node.nodeName;
},
/**
* Fixes the selection range, normalising it, ordering the start and end by dom position, and correcting for noteType and offset
*
* Note about offsets:
* If the container is a Node of type Text, Comment, or CDATASection, then the offset is the number of characters from the
* start of the container to the boundary point of the Range.
* For other Node types, the offset is the number of child nodes between the start of the container and the boundary
* point of the Range.
*
* @method _fix_selected_nodes_from_range
* @private
* @param {Node} a
* @param {integer} aoffset
* @param {Node} b
* @param {integer} boffset
* @returns {[Node, integer, Node, integer]}
*/
_fix_selected_nodes_from_range : function(a, aoffset, b, boffset) {
var first,
firstoffset,
second,
secondoffset,
searchfunc,
acheckend = true,
bcheckend = true,
checkend = true;
if (a.get('nodeType') === 1) {
a = a.get('childNodes').item(aoffset);
acheckend = false;
}
if (b.get('nodeType') === 1 && boffset > 0) {
b = b.get('childNodes').item(boffset - 1);
bcheckend = false;
}
if (a._yuid === b._yuid) {
if (aoffset < boffset) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
} else {
if (a.contains(b)) {
first = a;
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else if (b.contains(a)) {
first = b;
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
} else {
searchfunc = function find_first(nodes, x, y) {
var i, result;
for (i in nodes) {
if (nodes[i] === x.getDOMNode()) {
return x;
} else if (nodes[i] === y.getDOMNode()) {
return y;
} else if (nodes[i].nodeType === 1) {
result = find_first(nodes[i].childNodes, x, y);
if (result !== false) {
return result;
}
}
}
return false;
};
first = searchfunc(this._get_common_parent_from_nodes(a, b).get('childNodes'), a, b) || a;
if (first === a) {
firstoffset = aoffset;
second = b;
secondoffset = boffset;
checkend = bcheckend;
} else {
firstoffset = boffset;
second = a;
secondoffset = aoffset;
checkend = acheckend;
}
}
if (checkend && second.get('nodeType') === 3 && secondoffset === 0 && second.previous('*', true)) {
second = second.previous();
}
}
return [first, firstoffset, second, secondoffset];
},
/**
* Returns an array containing all of the nodes in the Dom between the two reference nodes.
*
* @method _get_all_nodes_between
* @private
* @param {Node} a
* @param {Node} b
* @param {Node} parent The common parent of a and b
* @returns {Node[]}
*/
_get_all_nodes_between : function(a, b, parent) {
var startfound = false,
endfound = false,
nodes = [],
parenthasselectedtext = false;
if (a === b || a.contains(b)) {
return [a];
}
if (b.contains(a)) {
return [b];
}
if (!parent) {
return [a, b];
}
nodes.push(a);
parent.get('childNodes').each(function(node) {
if (endfound) {
return;
}
if (!startfound && node === a) {
startfound = true;
} else if (startfound && !endfound) {
if (node === b) {
endfound = true;
} else {
if (node.get('nodeType') === 3) {
parenthasselectedtext = true;
} else {
nodes.push(node);
nodes = nodes.compact(node.all('*'));
}
}
}
});
nodes.push(b);
if (parenthasselectedtext) {
nodes.unshift(parent);
}
return nodes;
},
/**
* Returns the common parent of the two given nodes.
*
* @method _get_common_parent_from_nodes
* @private
* @param {Node} a
* @param {Node} b
* @returns {Node}
*/
_get_common_parent_from_nodes : function(a, b) {
var aparents = a.ancestors('*', false, '.' + CSS.CONTENT),
bparents = b.ancestors('*', false, '.' + CSS.CONTENT),
node;
while ((node = aparents.pop())) {
if (bparents.indexOf(node) !== -1) {
return node;
}
}
return a.ancestor('.' + CSS.CONTENT);
},
/**
* Get the dom node representing the common anscestor of the selection nodes.
* @return DOMNode or false
*/
get_selection_parent_node : function() {
var selection = M.editor_atto.get_selection();
if (selection.length) {
selection = selection.pop();
var selection = rangy.getSelection();
if (selection.rangeCount) {
return selection.getRangeAt(0).commonAncestorContainer;
}
if (selection.commonAncestorContainer) {
return selection.commonAncestorContainer;
} else if (selection.parentElement) {
return selection.parentElement();
}
// No selection
return false;
},
/**
* Set the current selection. Used to restore a selection.
* @param rangy.Range[] ranges - List of ranges in selection.
*/
set_selection : function(selection) {
var sel, i;
if (window.getSelection) {
sel = window.getSelection();
sel.removeAllRanges();
for (i = 0; i < selection.length; i++) {
sel.addRange(selection[i]);
}
} else if (document.selection) {
// IE < 9
if (selection.select) {
selection.select();
}
}
set_selection : function(ranges) {
var selection = rangy.getSelection();
selection.setRanges(ranges);
},
/**
@@ -1550,19 +1221,20 @@ M.editor_atto = M.editor_atto || {
/**
* Inserts the given HTML into the editable content at the currently focused point.
* @param {String} html
* @param String html
*/
insert_html_at_focus_point: function(html) {
// We check document.selection here as the insertHTML exec command wan't available in IE until version 11.
// In IE document.selection was removed at from version 11. Making it an easy affordable check.
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
if (range.pasteHTML) {
range.pasteHTML(html);
}
} else {
// All other browsers are great!
document.execCommand('insertHTML', false, html);
var selection = rangy.getSelection(),
range,
node = Y.Node.create(html);
if (selection.rangeCount) {
range = selection.getRangeAt(0);
}
if (range) {
range.deleteContents();
range.insertNode(node.getDOMNode());
}
},
@@ -9,7 +9,8 @@
"event-simulate",
"event-custom",
"yui-throttle",
"moodle-core-notification-dialogue"
"moodle-core-notification-dialogue",
"moodle-editor_atto-rangy"
]
},
"moodle-editor_atto-editor-shortcut": {
+19
View File
@@ -0,0 +1,19 @@
{
"name": "moodle-editor_atto-rangy",
"builds": {
"moodle-editor_atto-rangy": {
"prependfiles": [
"rangy-core.js",
"rangy-cssclassapplier.js",
"rangy-selectionsaverestore.js",
"rangy-serializer.js"
],
"jsfiles": [
"init.js"
]
}
},
"shifter": {
"lint": false
}
}
+18
View File
@@ -0,0 +1,18 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
if (!rangy.initialized) {
rangy.init();
}
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2010 Tim Down
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,713 @@
/**
* @license CSS Class Applier module for Rangy.
* Adds, removes and toggles CSS classes on Ranges and Selections
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
*/
rangy.createModule("CssClassApplier", function(api, module) {
api.requireModules( ["WrappedSelection", "WrappedRange"] );
var dom = api.dom;
var defaultTagName = "span";
function trim(str) {
return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
}
function hasClass(el, cssClass) {
return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
}
function addClass(el, cssClass) {
if (el.className) {
if (!hasClass(el, cssClass)) {
el.className += " " + cssClass;
}
} else {
el.className = cssClass;
}
}
var removeClass = (function() {
function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
}
return function(el, cssClass) {
if (el.className) {
el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
}
};
})();
function sortClassName(className) {
return className.split(/\s+/).sort().join(" ");
}
function getSortedClassName(el) {
return sortClassName(el.className);
}
function haveSameClasses(el1, el2) {
return getSortedClassName(el1) == getSortedClassName(el2);
}
function replaceWithOwnChildren(el) {
var parent = el.parentNode;
while (el.hasChildNodes()) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
function rangeSelectsAnyText(range, textNode) {
var textRange = range.cloneRange();
textRange.selectNodeContents(textNode);
var intersectionRange = textRange.intersection(range);
var text = intersectionRange ? intersectionRange.toString() : "";
textRange.detach();
return text != "";
}
function getEffectiveTextNodes(range) {
return range.getNodes([3], function(textNode) {
return rangeSelectsAnyText(range, textNode);
});
}
function elementsHaveSameNonClassAttributes(el1, el2) {
if (el1.attributes.length != el2.attributes.length) return false;
for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
attr1 = el1.attributes[i];
name = attr1.name;
if (name != "class") {
attr2 = el2.attributes.getNamedItem(name);
if (attr1.specified != attr2.specified) return false;
if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
}
}
return true;
}
function elementHasNonClassAttributes(el, exceptions) {
for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
attrName = el.attributes[i].name;
if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
return true;
}
}
return false;
}
function elementHasProps(el, props) {
for (var p in props) {
if (props.hasOwnProperty(p) && el[p] !== props[p]) {
return false;
}
}
return true;
}
var getComputedStyleProperty;
if (typeof window.getComputedStyle != "undefined") {
getComputedStyleProperty = function(el, propName) {
return dom.getWindow(el).getComputedStyle(el, null)[propName];
};
} else if (typeof document.documentElement.currentStyle != "undefined") {
getComputedStyleProperty = function(el, propName) {
return el.currentStyle[propName];
};
} else {
module.fail("No means of obtaining computed style properties found");
}
var isEditableElement;
(function() {
var testEl = document.createElement("div");
if (typeof testEl.isContentEditable == "boolean") {
isEditableElement = function(node) {
return node && node.nodeType == 1 && node.isContentEditable;
};
} else {
isEditableElement = function(node) {
if (!node || node.nodeType != 1 || node.contentEditable == "false") {
return false;
}
return node.contentEditable == "true" || isEditableElement(node.parentNode);
};
}
})();
function isEditingHost(node) {
var parent;
return node && node.nodeType == 1
&& (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
|| (isEditableElement(node) && !isEditableElement(node.parentNode)));
}
function isEditable(node) {
return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
}
var inlineDisplayRegex = /^inline(-block|-table)?$/i;
function isNonInlineElement(node) {
return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
}
// White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
function isUnrenderedWhiteSpaceNode(node) {
if (node.data.length == 0) {
return true;
}
if (htmlNonWhiteSpaceRegex.test(node.data)) {
return false;
}
var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
switch (cssWhiteSpace) {
case "pre":
case "pre-wrap":
case "-moz-pre-wrap":
return false;
case "pre-line":
if (/[\r\n]/.test(node.data)) {
return false;
}
}
// We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
// non-inline element, it will not be rendered. This seems to be a good enough definition.
return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
}
function isSplitPoint(node, offset) {
if (dom.isCharacterDataNode(node)) {
if (offset == 0) {
return !!node.previousSibling;
} else if (offset == node.length) {
return !!node.nextSibling;
} else {
return true;
}
}
return offset > 0 && offset < node.childNodes.length;
}
function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
var newNode;
var splitAtStart = (descendantOffset == 0);
if (dom.isAncestorOf(descendantNode, node)) {
return node;
}
if (dom.isCharacterDataNode(descendantNode)) {
if (descendantOffset == 0) {
descendantOffset = dom.getNodeIndex(descendantNode);
descendantNode = descendantNode.parentNode;
} else if (descendantOffset == descendantNode.length) {
descendantOffset = dom.getNodeIndex(descendantNode) + 1;
descendantNode = descendantNode.parentNode;
} else {
throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
+ descendantOffset + " in " + descendantNode.data);
}
}
if (isSplitPoint(descendantNode, descendantOffset)) {
if (!newNode) {
newNode = descendantNode.cloneNode(false);
if (newNode.id) {
newNode.removeAttribute("id");
}
var child;
while ((child = descendantNode.childNodes[descendantOffset])) {
newNode.appendChild(child);
}
dom.insertAfter(newNode, descendantNode);
}
return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
} else if (node != descendantNode) {
newNode = descendantNode.parentNode;
// Work out a new split point in the parent node
var newNodeIndex = dom.getNodeIndex(descendantNode);
if (!splitAtStart) {
newNodeIndex++;
}
return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
}
return node;
}
function areElementsMergeable(el1, el2) {
return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
}
function createAdjacentMergeableTextNodeGetter(forward) {
var propName = forward ? "nextSibling" : "previousSibling";
return function(textNode, checkParentElement) {
var el = textNode.parentNode;
var adjacentNode = textNode[propName];
if (adjacentNode) {
// Can merge if the node's previous/next sibling is a text node
if (adjacentNode && adjacentNode.nodeType == 3) {
return adjacentNode;
}
} else if (checkParentElement) {
// Compare text node parent element with its sibling
adjacentNode = el[propName];
if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
return adjacentNode[forward ? "firstChild" : "lastChild"];
}
}
return null;
}
}
var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
function Merge(firstNode) {
this.isElementMerge = (firstNode.nodeType == 1);
this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
this.textNodes = [this.firstTextNode];
}
Merge.prototype = {
doMerge: function() {
var textBits = [], textNode, parent, text;
for (var i = 0, len = this.textNodes.length; i < len; ++i) {
textNode = this.textNodes[i];
parent = textNode.parentNode;
textBits[i] = textNode.data;
if (i) {
parent.removeChild(textNode);
if (!parent.hasChildNodes()) {
parent.parentNode.removeChild(parent);
}
}
}
this.firstTextNode.data = text = textBits.join("");
return text;
},
getLength: function() {
var i = this.textNodes.length, len = 0;
while (i--) {
len += this.textNodes[i].length;
}
return len;
},
toString: function() {
var textBits = [];
for (var i = 0, len = this.textNodes.length; i < len; ++i) {
textBits[i] = "'" + this.textNodes[i].data + "'";
}
return "[Merge(" + textBits.join(",") + ")]";
}
};
var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
// Allow "class" as a property name in object properties
var mappedPropertyNames = {"class" : "className"};
function CssClassApplier(cssClass, options, tagNames) {
this.cssClass = cssClass;
var normalize, i, len, propName;
var elementPropertiesFromOptions = null;
// Initialize from options object
if (typeof options == "object" && options !== null) {
tagNames = options.tagNames;
elementPropertiesFromOptions = options.elementProperties;
for (i = 0; propName = optionProperties[i++]; ) {
if (options.hasOwnProperty(propName)) {
this[propName] = options[propName];
}
}
normalize = options.normalize;
} else {
normalize = options;
}
// Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
this.normalize = (typeof normalize == "undefined") ? true : normalize;
// Initialize element properties and attribute exceptions
this.attrExceptions = [];
var el = document.createElement(this.elementTagName);
this.elementProperties = {};
for (var p in elementPropertiesFromOptions) {
if (elementPropertiesFromOptions.hasOwnProperty(p)) {
// Map "class" to "className"
if (mappedPropertyNames.hasOwnProperty(p)) {
p = mappedPropertyNames[p];
}
el[p] = elementPropertiesFromOptions[p];
// Copy the property back from the dummy element so that later comparisons to check whether elements
// may be removed are checking against the right value. For example, the href property of an element
// returns a fully qualified URL even if it was previously assigned a relative URL.
this.elementProperties[p] = el[p];
this.attrExceptions.push(p);
}
}
this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
// Initialize tag names
this.applyToAnyTagName = false;
var type = typeof tagNames;
if (type == "string") {
if (tagNames == "*") {
this.applyToAnyTagName = true;
} else {
this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
}
} else if (type == "object" && typeof tagNames.length == "number") {
this.tagNames = [];
for (i = 0, len = tagNames.length; i < len; ++i) {
if (tagNames[i] == "*") {
this.applyToAnyTagName = true;
} else {
this.tagNames.push(tagNames[i].toLowerCase());
}
}
} else {
this.tagNames = [this.elementTagName];
}
}
CssClassApplier.prototype = {
elementTagName: defaultTagName,
elementProperties: {},
ignoreWhiteSpace: true,
applyToEditableOnly: false,
hasClass: function(node) {
return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
},
getSelfOrAncestorWithClass: function(node) {
while (node) {
if (this.hasClass(node, this.cssClass)) {
return node;
}
node = node.parentNode;
}
return null;
},
isModifiable: function(node) {
return !this.applyToEditableOnly || isEditable(node);
},
// White space adjacent to an unwrappable node can be ignored for wrapping
isIgnorableWhiteSpaceNode: function(node) {
return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
},
// Normalizes nodes after applying a CSS class to a Range.
postApply: function(textNodes, range, isUndo) {
var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
var merges = [], currentMerge;
var rangeStartNode = firstNode, rangeEndNode = lastNode;
var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
var textNode, precedingTextNode;
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
if (precedingTextNode) {
if (!currentMerge) {
currentMerge = new Merge(precedingTextNode);
merges.push(currentMerge);
}
currentMerge.textNodes.push(textNode);
if (textNode === firstNode) {
rangeStartNode = currentMerge.firstTextNode;
rangeStartOffset = rangeStartNode.length;
}
if (textNode === lastNode) {
rangeEndNode = currentMerge.firstTextNode;
rangeEndOffset = currentMerge.getLength();
}
} else {
currentMerge = null;
}
}
// Test whether the first node after the range needs merging
var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
if (nextTextNode) {
if (!currentMerge) {
currentMerge = new Merge(lastNode);
merges.push(currentMerge);
}
currentMerge.textNodes.push(nextTextNode);
}
// Do the merges
if (merges.length) {
for (i = 0, len = merges.length; i < len; ++i) {
merges[i].doMerge();
}
// Set the range boundaries
range.setStart(rangeStartNode, rangeStartOffset);
range.setEnd(rangeEndNode, rangeEndOffset);
}
},
createContainer: function(doc) {
var el = doc.createElement(this.elementTagName);
api.util.extend(el, this.elementProperties);
addClass(el, this.cssClass);
return el;
},
applyToTextNode: function(textNode) {
var parent = textNode.parentNode;
if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
addClass(parent, this.cssClass);
} else {
var el = this.createContainer(dom.getDocument(textNode));
textNode.parentNode.insertBefore(el, textNode);
el.appendChild(textNode);
}
},
isRemovable: function(el) {
return el.tagName.toLowerCase() == this.elementTagName
&& getSortedClassName(el) == this.elementSortedClassName
&& elementHasProps(el, this.elementProperties)
&& !elementHasNonClassAttributes(el, this.attrExceptions)
&& this.isModifiable(el);
},
undoToTextNode: function(textNode, range, ancestorWithClass) {
if (!range.containsNode(ancestorWithClass)) {
// Split out the portion of the ancestor from which we can remove the CSS class
//var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
var ancestorRange = range.cloneRange();
ancestorRange.selectNode(ancestorWithClass);
if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
range.setEndAfter(ancestorWithClass);
}
if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
}
}
if (this.isRemovable(ancestorWithClass)) {
replaceWithOwnChildren(ancestorWithClass);
} else {
removeClass(ancestorWithClass, this.cssClass);
}
},
applyToRange: function(range) {
range.splitBoundaries();
var textNodes = getEffectiveTextNodes(range);
if (textNodes.length) {
var textNode;
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
&& this.isModifiable(textNode)) {
this.applyToTextNode(textNode);
}
}
range.setStart(textNodes[0], 0);
textNode = textNodes[textNodes.length - 1];
range.setEnd(textNode, textNode.length);
if (this.normalize) {
this.postApply(textNodes, range, false);
}
}
},
applyToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var range, ranges = sel.getAllRanges();
sel.removeAllRanges();
var i = ranges.length;
while (i--) {
range = ranges[i];
this.applyToRange(range);
sel.addRange(range);
}
},
undoToRange: function(range) {
range.splitBoundaries();
var textNodes = getEffectiveTextNodes(range);
var textNode, ancestorWithClass;
var lastTextNode = textNodes[textNodes.length - 1];
if (textNodes.length) {
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
if (ancestorWithClass && this.isModifiable(textNode)) {
this.undoToTextNode(textNode, range, ancestorWithClass);
}
// Ensure the range is still valid
range.setStart(textNodes[0], 0);
range.setEnd(lastTextNode, lastTextNode.length);
}
if (this.normalize) {
this.postApply(textNodes, range, true);
}
}
},
undoToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var ranges = sel.getAllRanges(), range;
sel.removeAllRanges();
for (var i = 0, len = ranges.length; i < len; ++i) {
range = ranges[i];
this.undoToRange(range);
sel.addRange(range);
}
},
getTextSelectedByRange: function(textNode, range) {
var textRange = range.cloneRange();
textRange.selectNodeContents(textNode);
var intersectionRange = textRange.intersection(range);
var text = intersectionRange ? intersectionRange.toString() : "";
textRange.detach();
return text;
},
isAppliedToRange: function(range) {
if (range.collapsed) {
return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
} else {
var textNodes = range.getNodes( [3] );
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
&& this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
return false;
}
}
return true;
}
},
isAppliedToSelection: function(win) {
win = win || window;
var sel = api.getSelection(win);
var ranges = sel.getAllRanges();
var i = ranges.length;
while (i--) {
if (!this.isAppliedToRange(ranges[i])) {
return false;
}
}
return true;
},
toggleRange: function(range) {
if (this.isAppliedToRange(range)) {
this.undoToRange(range);
} else {
this.applyToRange(range);
}
},
toggleSelection: function(win) {
if (this.isAppliedToSelection(win)) {
this.undoToSelection(win);
} else {
this.applyToSelection(win);
}
},
detach: function() {}
};
function createCssClassApplier(cssClass, options, tagNames) {
return new CssClassApplier(cssClass, options, tagNames);
}
CssClassApplier.util = {
hasClass: hasClass,
addClass: addClass,
removeClass: removeClass,
hasSameClasses: haveSameClasses,
replaceWithOwnChildren: replaceWithOwnChildren,
elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
elementHasNonClassAttributes: elementHasNonClassAttributes,
splitNodeAt: splitNodeAt,
isEditableElement: isEditableElement,
isEditingHost: isEditingHost,
isEditable: isEditable
};
api.CssClassApplier = CssClassApplier;
api.createCssClassApplier = createCssClassApplier;
});
@@ -0,0 +1,195 @@
/**
* @license Selection save and restore module for Rangy.
* Saves and restores user selections using marker invisible elements in the DOM.
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
*/
rangy.createModule("SaveRestore", function(api, module) {
api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
var dom = api.dom;
var markerTextChar = "\ufeff";
function gEBI(id, doc) {
return (doc || document).getElementById(id);
}
function insertRangeBoundaryMarker(range, atStart) {
var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
var markerEl;
var doc = dom.getDocument(range.startContainer);
// Clone the Range and collapse to the appropriate boundary point
var boundaryRange = range.cloneRange();
boundaryRange.collapse(atStart);
// Create the marker element containing a single invisible character using DOM methods and insert it
markerEl = doc.createElement("span");
markerEl.id = markerId;
markerEl.style.lineHeight = "0";
markerEl.style.display = "none";
markerEl.className = "rangySelectionBoundary";
markerEl.appendChild(doc.createTextNode(markerTextChar));
boundaryRange.insertNode(markerEl);
boundaryRange.detach();
return markerEl;
}
function setRangeBoundary(doc, range, markerId, atStart) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
markerEl.parentNode.removeChild(markerEl);
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
}
function compareRanges(r1, r2) {
return r2.compareBoundaryPoints(r1.START_TO_START, r1);
}
function saveSelection(win) {
win = win || window;
var doc = win.document;
if (!api.isSelectionValid(win)) {
module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
return;
}
var sel = api.getSelection(win);
var ranges = sel.getAllRanges();
var rangeInfos = [], startEl, endEl, range;
// Order the ranges by position within the DOM, latest first
ranges.sort(compareRanges);
for (var i = 0, len = ranges.length; i < len; ++i) {
range = ranges[i];
if (range.collapsed) {
endEl = insertRangeBoundaryMarker(range, false);
rangeInfos.push({
markerId: endEl.id,
collapsed: true
});
} else {
endEl = insertRangeBoundaryMarker(range, false);
startEl = insertRangeBoundaryMarker(range, true);
rangeInfos[i] = {
startMarkerId: startEl.id,
endMarkerId: endEl.id,
collapsed: false,
backwards: ranges.length == 1 && sel.isBackwards()
};
}
}
// Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
// between its markers
for (i = len - 1; i >= 0; --i) {
range = ranges[i];
if (range.collapsed) {
range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
} else {
range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
}
}
// Ensure current selection is unaffected
sel.setRanges(ranges);
return {
win: win,
doc: doc,
rangeInfos: rangeInfos,
restored: false
};
}
function restoreSelection(savedSelection, preserveDirection) {
if (!savedSelection.restored) {
var rangeInfos = savedSelection.rangeInfos;
var sel = api.getSelection(savedSelection.win);
var ranges = [];
// Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
// normalization affecting previously restored ranges.
for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
rangeInfo = rangeInfos[i];
range = api.createRange(savedSelection.doc);
if (rangeInfo.collapsed) {
var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
if (markerEl) {
markerEl.style.display = "inline";
var previousNode = markerEl.previousSibling;
// Workaround for issue 17
if (previousNode && previousNode.nodeType == 3) {
markerEl.parentNode.removeChild(markerEl);
range.collapseToPoint(previousNode, previousNode.length);
} else {
range.collapseBefore(markerEl);
markerEl.parentNode.removeChild(markerEl);
}
} else {
module.warn("Marker element has been removed. Cannot restore selection.");
}
} else {
setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
}
// Normalizing range boundaries is only viable if the selection contains only one range. For example,
// if the selection contained two ranges that were both contained within the same single text node,
// both would alter the same text node when restoring and break the other range.
if (len == 1) {
range.normalizeBoundaries();
}
ranges[i] = range;
}
if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
sel.removeAllRanges();
sel.addRange(ranges[0], true);
} else {
sel.setRanges(ranges);
}
savedSelection.restored = true;
}
}
function removeMarkerElement(doc, markerId) {
var markerEl = gEBI(markerId, doc);
if (markerEl) {
markerEl.parentNode.removeChild(markerEl);
}
}
function removeMarkers(savedSelection) {
var rangeInfos = savedSelection.rangeInfos;
for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
rangeInfo = rangeInfos[i];
if (rangeInfo.collapsed) {
removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
} else {
removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
}
}
}
api.saveSelection = saveSelection;
api.restoreSelection = restoreSelection;
api.removeMarkerElement = removeMarkerElement;
api.removeMarkers = removeMarkers;
});
+300
View File
@@ -0,0 +1,300 @@
/**
* @license Serializer module for Rangy.
* Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
* cookie or local storage and restore it on the user's next visit to the same page.
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2012, Tim Down
* Licensed under the MIT license.
* Version: 1.2.3
* Build date: 26 February 2012
*/
rangy.createModule("Serializer", function(api, module) {
api.requireModules( ["WrappedSelection", "WrappedRange"] );
var UNDEF = "undefined";
// encodeURIComponent and decodeURIComponent are required for cookie handling
if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
}
// Checksum for checking whether range can be serialized
var crc32 = (function() {
function utf8encode(str) {
var utf8CharCodes = [];
for (var i = 0, len = str.length, c; i < len; ++i) {
c = str.charCodeAt(i);
if (c < 128) {
utf8CharCodes.push(c);
} else if (c < 2048) {
utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
} else {
utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
}
}
return utf8CharCodes;
}
var cachedCrcTable = null;
function buildCRCTable() {
var table = [];
for (var i = 0, j, crc; i < 256; ++i) {
crc = i;
j = 8;
while (j--) {
if ((crc & 1) == 1) {
crc = (crc >>> 1) ^ 0xEDB88320;
} else {
crc >>>= 1;
}
}
table[i] = crc >>> 0;
}
return table;
}
function getCrcTable() {
if (!cachedCrcTable) {
cachedCrcTable = buildCRCTable();
}
return cachedCrcTable;
}
return function(str) {
var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
y = (crc ^ utf8CharCodes[i]) & 0xFF;
crc = (crc >>> 8) ^ crcTable[y];
}
return (crc ^ -1) >>> 0;
};
})();
var dom = api.dom;
function escapeTextForHtml(str) {
return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function nodeToInfoString(node, infoParts) {
infoParts = infoParts || [];
var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
var start = "", end = "";
switch (nodeType) {
case 3: // Text node
start = escapeTextForHtml(node.nodeValue);
break;
case 8: // Comment
start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
break;
default:
start = "<" + nodeInfo + ">";
end = "</>";
break;
}
if (start) {
infoParts.push(start);
}
for (var i = 0; i < childCount; ++i) {
nodeToInfoString(children[i], infoParts);
}
if (end) {
infoParts.push(end);
}
return infoParts;
}
// Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
// attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
// IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
// innerHTML whenever the user changes an input within the element.
function getElementChecksum(el) {
var info = nodeToInfoString(el).join("");
return crc32(info).toString(16);
}
function serializePosition(node, offset, rootNode) {
var pathBits = [], n = node;
rootNode = rootNode || dom.getDocument(node).documentElement;
while (n && n != rootNode) {
pathBits.push(dom.getNodeIndex(n, true));
n = n.parentNode;
}
return pathBits.join("/") + ":" + offset;
}
function deserializePosition(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var bits = serialized.split(":");
var node = rootNode;
var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
while (i--) {
nodeIndex = parseInt(nodeIndices[i], 10);
if (nodeIndex < node.childNodes.length) {
node = node.childNodes[parseInt(nodeIndices[i], 10)];
} else {
throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
" has no child with index " + nodeIndex + ", " + i);
}
}
return new dom.DomPosition(node, parseInt(bits[1], 10));
}
function serializeRange(range, omitChecksum, rootNode) {
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
throw new Error("serializeRange: range is not wholly contained within specified root node");
}
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
serializePosition(range.endContainer, range.endOffset, rootNode);
if (!omitChecksum) {
serialized += "{" + getElementChecksum(rootNode) + "}";
}
return serialized;
}
function deserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
if (checksum && checksum !== getElementChecksum(rootNode)) {
throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
") and target root node (" + rootNodeChecksum + ") do not match");
}
var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
var range = api.createRange(doc);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
return range;
}
function canDeserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[3];
return !checksum || checksum === getElementChecksum(rootNode);
}
function serializeSelection(selection, omitChecksum, rootNode) {
selection = selection || api.getSelection();
var ranges = selection.getAllRanges(), serializedRanges = [];
for (var i = 0, len = ranges.length; i < len; ++i) {
serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
}
return serializedRanges.join("|");
}
function deserializeSelection(serialized, rootNode, win) {
if (rootNode) {
win = win || dom.getWindow(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
var sel = api.getSelection(win);
var ranges = [];
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
}
sel.setRanges(ranges);
return sel;
}
function canDeserializeSelection(serialized, rootNode, win) {
var doc;
if (rootNode) {
doc = win ? win.document : dom.getDocument(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
return false;
}
}
return true;
}
var cookieName = "rangySerializedSelection";
function getSerializedSelectionFromCookie(cookie) {
var parts = cookie.split(/[;,]/);
for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
nameVal = parts[i].split("=");
if (nameVal[0].replace(/^\s+/, "") == cookieName) {
val = nameVal[1];
if (val) {
return decodeURIComponent(val.replace(/\s+$/, ""));
}
}
}
return null;
}
function restoreSelectionFromCookie(win) {
win = win || window;
var serialized = getSerializedSelectionFromCookie(win.document.cookie);
if (serialized) {
deserializeSelection(serialized, win.doc)
}
}
function saveSelectionCookie(win, props) {
win = win || window;
props = (typeof props == "object") ? props : {};
var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
var path = props.path ? ";path=" + props.path : "";
var domain = props.domain ? ";domain=" + props.domain : "";
var secure = props.secure ? ";secure" : "";
var serialized = serializeSelection(api.getSelection(win));
win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
}
api.serializePosition = serializePosition;
api.deserializePosition = deserializePosition;
api.serializeRange = serializeRange;
api.deserializeRange = deserializeRange;
api.canDeserializeRange = canDeserializeRange;
api.serializeSelection = serializeSelection;
api.deserializeSelection = deserializeSelection;
api.canDeserializeSelection = canDeserializeSelection;
api.restoreSelectionFromCookie = restoreSelectionFromCookie;
api.saveSelectionCookie = saveSelectionCookie;
api.getElementChecksum = getElementChecksum;
});
@@ -0,0 +1,5 @@
{
"moodle-editor_atto-rangy": {
"requires": [ ]
}
}