Файл: library/wysihtml5/src/selection/html_applier.js
Строк: 636
<?php
/**
* Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
* http://code.google.com/p/rangy/
*
* changed in order to be able ...
* - to use custom tags
* - to detect and replace similar css classes via reg exp
*/
(function(wysihtml5, rangy) {
var defaultTagName = "span";
var REG_EXP_WHITE_SPACE = /s+/g;
function hasClass(el, cssClass, regExp) {
if (!el.className) {
return false;
}
var matchingClassNames = el.className.match(regExp) || [];
return matchingClassNames[matchingClassNames.length - 1] === cssClass;
}
function addClass(el, cssClass, regExp) {
if (el.className) {
removeClass(el, regExp);
el.className += " " + cssClass;
} else {
el.className = cssClass;
}
}
function removeClass(el, regExp) {
if (el.className) {
el.className = el.className.replace(regExp, "");
}
}
function hasSameClasses(el1, el2) {
return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
}
function replaceWithOwnChildren(el) {
var parent = el.parentNode;
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
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 isSplitPoint(node, offset) {
if (rangy.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) {
var newNode;
if (rangy.dom.isCharacterDataNode(descendantNode)) {
if (descendantOffset == 0) {
descendantOffset = rangy.dom.getNodeIndex(descendantNode);
descendantNode = descendantNode.parentNode;
} else if (descendantOffset == descendantNode.length) {
descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
descendantNode = descendantNode.parentNode;
} else {
newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
}
}
if (!newNode) {
newNode = descendantNode.cloneNode(false);
if (newNode.id) {
newNode.removeAttribute("id");
}
var child;
while ((child = descendantNode.childNodes[descendantOffset])) {
newNode.appendChild(child);
}
rangy.dom.insertAfter(newNode, descendantNode);
}
return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
}
function Merge(firstNode) {
this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
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(",") + ")]";
}
};
function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
this.tagNames = tagNames || [defaultTagName];
this.cssClass = cssClass || "";
this.similarClassRegExp = similarClassRegExp;
this.normalize = normalize;
this.applyToAnyTagName = false;
}
HTMLApplier.prototype = {
getAncestorWithClass: function(node) {
var cssClassMatch;
while (node) {
cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
return node;
}
node = node.parentNode;
}
return false;
},
// Normalizes nodes after applying a CSS class to a Range.
postApply: function(textNodes, range) {
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 = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
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 = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
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);
}
},
getAdjacentMergeableTextNode: function(node, forward) {
var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
var el = isTextNode ? node.parentNode : node;
var adjacentNode;
var propName = forward ? "nextSibling" : "previousSibling";
if (isTextNode) {
// Can merge if the node's previous/next sibling is a text node
adjacentNode = node[propName];
if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
return adjacentNode;
}
} else {
// Compare element with its sibling
adjacentNode = el[propName];
if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
return adjacentNode[forward ? "firstChild" : "lastChild"];
}
}
return null;
},
areElementsMergeable: function(el1, el2) {
return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
&& rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
&& hasSameClasses(el1, el2)
&& elementsHaveSameNonClassAttributes(el1, el2);
},
createContainer: function(doc) {
var el = doc.createElement(this.tagNames[0]);
if (this.cssClass) {
el.className = this.cssClass;
}
return el;
},
applyToTextNode: function(textNode) {
var parent = textNode.parentNode;
if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
if (this.cssClass) {
addClass(parent, this.cssClass, this.similarClassRegExp);
}
} else {
var el = this.createContainer(rangy.dom.getDocument(textNode));
textNode.parentNode.insertBefore(el, textNode);
el.appendChild(textNode);
}
},
isRemovable: function(el) {
return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
},
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 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.setEndAfter(ancestorWithClass);
}
if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
}
}
if (this.similarClassRegExp) {
removeClass(ancestorWithClass, this.similarClassRegExp);
}
if (this.isRemovable(ancestorWithClass)) {
replaceWithOwnChildren(ancestorWithClass);
}
},
applyToRange: function(range) {
var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
if (!textNodes.length) {
try {
var node = this.createContainer(range.endContainer.ownerDocument);
range.surroundContents(node);
this.selectNode(range, node);
return;
} catch(e) {}
}
range.splitBoundaries();
textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
if (textNodes.length) {
var textNode;
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
if (!this.getAncestorWithClass(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);
}
}
},
undoToRange: function(range) {
var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
if (textNodes.length) {
range.splitBoundaries();
textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
} else {
var doc = range.endContainer.ownerDocument,
node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
range.insertNode(node);
range.selectNode(node);
textNodes = [node];
}
for (var i = 0, len = textNodes.length; i < len; ++i) {
textNode = textNodes[i];
ancestorWithClass = this.getAncestorWithClass(textNode);
if (ancestorWithClass) {
this.undoToTextNode(textNode, range, ancestorWithClass);
}
}
if (len == 1) {
this.selectNode(range, textNodes[0]);
} else {
range.setStart(textNodes[0], 0);
textNode = textNodes[textNodes.length - 1];
range.setEnd(textNode, textNode.length);
if (this.normalize) {
this.postApply(textNodes, range);
}
}
},
selectNode: function(range, node) {
var isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true,
content = isElement ? node.innerHTML : node.data,
isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
if (isEmpty && isElement && canHaveHTML) {
// Make sure that caret is visible in node by inserting a zero width no breaking space
try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
}
range.selectNodeContents(node);
if (isEmpty && isElement) {
range.collapse(false);
} else if (isEmpty) {
range.setStartAfter(node);
range.setEndAfter(node);
}
},
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) {
var ancestors = [],
ancestor,
textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
if (!textNodes.length) {
ancestor = this.getAncestorWithClass(range.startContainer);
return ancestor ? [ancestor] : false;
}
for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
selectedText = this.getTextSelectedByRange(textNodes[i], range);
ancestor = this.getAncestorWithClass(textNodes[i]);
if (selectedText != "" && !ancestor) {
return false;
} else {
ancestors.push(ancestor);
}
}
return ancestors;
},
toggleRange: function(range) {
if (this.isAppliedToRange(range)) {
this.undoToRange(range);
} else {
this.applyToRange(range);
}
}
};
wysihtml5.selection.HTMLApplier = HTMLApplier;
})(wysihtml5, rangy);
?>