Файл: library/wysihtml5/src/selection/selection.js
Строк: 683
<?php
/**
* Selection API
*
* @example
* var selection = new wysihtml5.Selection(editor);
*/
(function(wysihtml5) {
var dom = wysihtml5.dom;
function _getCumulativeOffsetTop(element) {
var top = 0;
if (element.parentNode) {
do {
top += element.offsetTop || 0;
element = element.offsetParent;
} while (element);
}
return top;
}
wysihtml5.Selection = Base.extend(
/** @scope wysihtml5.Selection.prototype */ {
constructor: function(editor) {
// Make sure that our external range library is initialized
window.rangy.init();
this.editor = editor;
this.composer = editor.composer;
this.doc = this.composer.doc;
},
/**
* Get the current selection as a bookmark to be able to later restore it
*
* @return {Object} An object that represents the current selection
*/
getBookmark: function() {
var range = this.getRange();
return range && range.cloneRange();
},
/**
* Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
*
* @param {Object} bookmark An object that represents the current selection
*/
setBookmark: function(bookmark) {
if (!bookmark) {
return;
}
this.setSelection(bookmark);
},
/**
* Set the caret in front of the given node
*
* @param {Object} node The element or text node where to position the caret in front of
* @example
* selection.setBefore(myElement);
*/
setBefore: function(node) {
var range = rangy.createRange(this.doc);
range.setStartBefore(node);
range.setEndBefore(node);
return this.setSelection(range);
},
/**
* Set the caret after the given node
*
* @param {Object} node The element or text node where to position the caret in front of
* @example
* selection.setBefore(myElement);
*/
setAfter: function(node) {
var range = rangy.createRange(this.doc);
range.setStartAfter(node);
range.setEndAfter(node);
return this.setSelection(range);
},
/**
* Ability to select/mark nodes
*
* @param {Element} node The node/element to select
* @example
* selection.selectNode(document.getElementById("my-image"));
*/
selectNode: function(node, avoidInvisibleSpace) {
var range = rangy.createRange(this.doc),
isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
content = isElement ? node.innerHTML : node.data,
isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
displayStyle = dom.getStyle("display").from(node),
isBlockElement = (displayStyle === "block" || displayStyle === "list-item");
if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
// Make sure that caret is visible in node by inserting a zero width no breaking space
try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
}
if (canHaveHTML) {
range.selectNodeContents(node);
} else {
range.selectNode(node);
}
if (canHaveHTML && isEmpty && isElement) {
range.collapse(isBlockElement);
} else if (canHaveHTML && isEmpty) {
range.setStartAfter(node);
range.setEndAfter(node);
}
this.setSelection(range);
},
/**
* Get the node which contains the selection
*
* @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
* @return {Object} The node that contains the caret
* @example
* var nodeThatContainsCaret = selection.getSelectedNode();
*/
getSelectedNode: function(controlRange) {
var selection,
range;
if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
range = this.doc.selection.createRange();
if (range && range.length) {
return range.item(0);
}
}
selection = this.getSelection(this.doc);
if (selection.focusNode === selection.anchorNode) {
return selection.focusNode;
} else {
range = this.getRange(this.doc);
return range ? range.commonAncestorContainer : this.doc.body;
}
},
executeAndRestore: function(method, restoreScrollPosition) {
var body = this.doc.body,
oldScrollTop = restoreScrollPosition && body.scrollTop,
oldScrollLeft = restoreScrollPosition && body.scrollLeft,
className = "_wysihtml5-temp-placeholder",
placeholderHtml = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
range = this.getRange(this.doc),
caretPlaceholder,
newCaretPlaceholder,
nextSibling,
node,
newRange;
// Nothing selected, execute and say goodbye
if (!range) {
method(body, body);
return;
}
if (wysihtml5.browser.hasInsertNodeIssue()) {
this.doc.execCommand("insertHTML", false, placeholderHtml);
} else {
node = range.createContextualFragment(placeholderHtml);
range.insertNode(node);
}
// Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
try {
method(range.startContainer, range.endContainer);
} catch(e) {
setTimeout(function() { throw e; }, 0);
}
caretPlaceholder = this.doc.querySelector("." + className);
if (caretPlaceholder) {
newRange = rangy.createRange(this.doc);
nextSibling = caretPlaceholder.nextSibling;
// Opera is so fucked up when you wanna set focus before a <br>
if (wysihtml5.browser.hasInsertNodeIssue() && nextSibling && nextSibling.nodeName === "BR") {
newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
dom.insert(newCaretPlaceholder).after(caretPlaceholder);
newRange.setStartBefore(newCaretPlaceholder);
newRange.setEndBefore(newCaretPlaceholder);
} else {
newRange.selectNode(caretPlaceholder);
newRange.deleteContents();
}
this.setSelection(newRange);
} else {
// fallback for when all hell breaks loose
body.focus();
}
if (restoreScrollPosition) {
body.scrollTop = oldScrollTop;
body.scrollLeft = oldScrollLeft;
}
// Remove it again, just to make sure that the placeholder is definitely out of the dom tree
try {
caretPlaceholder.parentNode.removeChild(caretPlaceholder);
} catch(e2) {}
},
/**
* Different approach of preserving the selection (doesn't modify the dom)
* Takes all text nodes in the selection and saves the selection position in the first and last one
*/
executeAndRestoreSimple: function(method) {
var range = this.getRange(),
body = this.doc.body,
newRange,
firstNode,
lastNode,
textNodes,
rangeBackup;
// Nothing selected, execute and say goodbye
if (!range) {
method(body, body);
return;
}
textNodes = range.getNodes([3]);
firstNode = textNodes[0] || range.startContainer;
lastNode = textNodes[textNodes.length - 1] || range.endContainer;
rangeBackup = {
collapsed: range.collapsed,
startContainer: firstNode,
startOffset: firstNode === range.startContainer ? range.startOffset : 0,
endContainer: lastNode,
endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length
};
try {
method(range.startContainer, range.endContainer);
} catch(e) {
setTimeout(function() { throw e; }, 0);
}
newRange = rangy.createRange(this.doc);
try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}
try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}
try { this.setSelection(newRange); } catch(e3) {}
},
set: function(node, offset) {
var newRange = rangy.createRange(this.doc);
newRange.setStart(node, offset || 0);
this.setSelection(newRange);
},
/**
* Insert html at the caret position and move the cursor after the inserted html
*
* @param {String} html HTML string to insert
* @example
* selection.insertHTML("<p>foobar</p>");
*/
insertHTML: function(html) {
var range = rangy.createRange(this.doc),
node = range.createContextualFragment(html),
lastChild = node.lastChild;
this.insertNode(node);
if (lastChild) {
this.setAfter(lastChild);
}
},
/**
* Insert a node at the caret position and move the cursor behind it
*
* @param {Object} node HTML string to insert
* @example
* selection.insertNode(document.createTextNode("foobar"));
*/
insertNode: function(node) {
var range = this.getRange();
if (range) {
range.insertNode(node);
}
},
/**
* Wraps current selection with the given node
*
* @param {Object} node The node to surround the selected elements with
*/
surround: function(node) {
var range = this.getRange();
if (!range) {
return;
}
try {
// This only works when the range boundaries are not overlapping other elements
range.surroundContents(node);
this.selectNode(node);
} catch(e) {
// fallback
node.appendChild(range.extractContents());
range.insertNode(node);
}
},
/**
* Scroll the current caret position into the view
* FIXME: This is a bit hacky, there might be a smarter way of doing this
*
* @example
* selection.scrollIntoView();
*/
scrollIntoView: function() {
var doc = this.doc,
tolerance = 5, // px
hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
var element = doc.createElement("span");
// The element needs content in order to be able to calculate it's position properly
element.innerHTML = wysihtml5.INVISIBLE_SPACE;
return element;
})(),
offsetTop;
if (hasScrollBars) {
this.insertNode(tempElement);
offsetTop = _getCumulativeOffsetTop(tempElement);
tempElement.parentNode.removeChild(tempElement);
if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
doc.body.scrollTop = offsetTop;
}
}
},
/**
* Select line where the caret is in
*/
selectLine: function() {
if (wysihtml5.browser.supportsSelectionModify()) {
this._selectLine_W3C();
} else if (this.doc.selection) {
this._selectLine_MSIE();
}
},
/**
* See https://developer.mozilla.org/en/DOM/Selection/modify
*/
_selectLine_W3C: function() {
var win = this.doc.defaultView,
selection = win.getSelection();
selection.modify("extend", "left", "lineboundary");
selection.modify("extend", "right", "lineboundary");
},
_selectLine_MSIE: function() {
var range = this.doc.selection.createRange(),
rangeTop = range.boundingTop,
scrollWidth = this.doc.body.scrollWidth,
rangeBottom,
rangeEnd,
measureNode,
i,
j;
if (!range.moveToPoint) {
return;
}
if (rangeTop === 0) {
// Don't know why, but when the selection ends at the end of a line
// range.boundingTop is 0
measureNode = this.doc.createElement("span");
this.insertNode(measureNode);
rangeTop = measureNode.offsetTop;
measureNode.parentNode.removeChild(measureNode);
}
rangeTop += 1;
for (i=-10; i<scrollWidth; i+=2) {
try {
range.moveToPoint(i, rangeTop);
break;
} catch(e1) {}
}
// Investigate the following in order to handle multi line selections
// rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
rangeBottom = rangeTop;
rangeEnd = this.doc.selection.createRange();
for (j=scrollWidth; j>=0; j--) {
try {
rangeEnd.moveToPoint(j, rangeBottom);
break;
} catch(e2) {}
}
range.setEndPoint("EndToEnd", rangeEnd);
range.select();
},
getText: function() {
var selection = this.getSelection();
return selection ? selection.toString() : "";
},
getNodes: function(nodeType, filter) {
var range = this.getRange();
if (range) {
return range.getNodes([nodeType], filter);
} else {
return [];
}
},
getRange: function() {
var selection = this.getSelection();
return selection && selection.rangeCount && selection.getRangeAt(0);
},
getSelection: function() {
return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
},
setSelection: function(range) {
var win = this.doc.defaultView || this.doc.parentWindow,
selection = rangy.getSelection(win);
return selection.setSingleRange(range);
}
});
})(wysihtml5);
?>