/*!
* This file is part of Aloha Editor Project http://aloha-editor.org
* Copyright © 2010-2011 Gentics Software GmbH, aloha@gentics.com
* Contributors http://aloha-editor.org/contribution.php
* Licensed unter the terms of http://www.aloha-editor.org/license.html
*//*
* Aloha Editor is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.*
*
* Aloha Editor 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see
* - merge: merges multiple successive nodes of same type, if this is allowed, starting at the children of the given node (defaults to false) * - removeempty: removes empty element nodes (defaults to false) ** Example for calling this method:
GENTICS.Utils.Dom.doCleanup({merge:true,removeempty:false}, range)
* @param {object} cleanup type of cleanup to be done
* @param {GENTICS.Utils.RangeObject} rangeObject range which is eventually updated
* @param {DOMObject} start start object, if not given, the commonancestorcontainer is used as startobject insted
* @return {boolean} true when the range (startContainer/startOffset/endContainer/endOffset) was modified, false if not
* @method
*/
doCleanup: function(cleanup, rangeObject, start) {
var that = this, prevNode, modifiedRange, startObject, startOffset, endOffset;
if (typeof cleanup === 'undefined') {
cleanup = {};
}
if (typeof cleanup.merge === 'undefined') {
cleanup.merge = false;
}
if (typeof cleanup.removeempty === 'undefined') {
cleanup.removeempty = false;
}
if (typeof start === 'undefined' && rangeObject) {
start = rangeObject.getCommonAncestorContainer();
}
// remember the previous node here (successive nodes of same type will be merged into this)
prevNode = false;
// check whether the range needed to be modified during merging
modifiedRange = false;
// get the start object
startObject = jQuery(start);
startOffset = rangeObject.startOffset;
endOffset = rangeObject.endOffset;
// iterate through all sub nodes
startObject.contents().each(function(index) {
// decide further actions by node type
switch(this.nodeType) {
// found a non-text node
case 1:
if (prevNode && prevNode.nodeName == this.nodeName) {
// found a successive node of same type
// now we check whether the selection starts or ends in the mother node after the current node
if (rangeObject.startContainer === startObject && startOffset > index) {
// there will be one less object, so reduce the startOffset by one
rangeObject.startOffset -= 1;
// set the flag for range modification
modifiedRange = true;
}
if (rangeObject.endContainer === startObject && endOffset > index) {
// there will be one less object, so reduce the endOffset by one
rangeObject.endOffset -= 1;
// set the flag for range modification
modifiedRange = true;
}
// merge the contents of this node into the previous one
jQuery(prevNode).append(jQuery(this).contents());
// after merging, we eventually need to cleanup the prevNode again
modifiedRange |= that.doCleanup(cleanup, rangeObject, prevNode);
// remove this node
jQuery(this).remove();
} else {
// do the recursion step here
modifiedRange |= that.doCleanup(cleanup, rangeObject, this);
// eventually remove empty elements
var removed = false;
if (cleanup.removeempty) {
if (GENTICS.Utils.Dom.isBlockLevelElement(this) && this.childNodes.length === 0) {
// jQuery(this).remove();
removed = true;
}
if (jQuery.inArray(this.nodeName.toLowerCase(), that.mergeableTags) >= 0
&& jQuery(this).text().length === 0 && this.childNodes.length === 0) {
// jQuery(this).remove();
removed = true;
}
}
// when the current node was not removed, we eventually store it as previous (mergeable) tag
if (!removed) {
if (jQuery.inArray(this.nodeName.toLowerCase(), that.mergeableTags) >= 0) {
prevNode = this;
} else {
prevNode = false;
}
} else {
// now we check whether the selection starts or ends in the mother node of this
if (rangeObject.startContainer === this.parentNode && startOffset > index) {
// there will be one less object, so reduce the startOffset by one
rangeObject.startOffset = rangeObject.startOffset - 1;
// set the flag for range modification
modifiedRange = true;
}
if (rangeObject.endContainer === this.parentNode && endOffset > index) {
// there will be one less object, so reduce the endOffset by one
rangeObject.endOffset = rangeObject.endOffset - 1;
// set the flag for range modification
modifiedRange = true;
}
// remove this text node
jQuery(this).remove();
}
}
break;
// found a text node
case 3:
// found a text node
if (prevNode && prevNode.nodeType === 3 && cleanup.merge) {
// the current text node will be merged into the last one, so
// check whether the selection starts or ends in the current
// text node
if (rangeObject.startContainer === this) {
// selection starts in the current text node
// update the start container to the last node
rangeObject.startContainer = prevNode;
// update the start offset
rangeObject.startOffset += prevNode.nodeValue.length;
// set the flag for range modification
modifiedRange = true;
} else if (rangeObject.startContainer === prevNode.parentNode
&& rangeObject.startOffset === that.getIndexInParent(prevNode) + 1) {
// selection starts right between the previous and current text nodes (which will be merged)
// update the start container to the previous node
rangeObject.startContainer = prevNode;
// set the start offset
rangeObject.startOffset = prevNode.nodeValue.length;
// set the flag for range modification
modifiedRange = true;
}
if (rangeObject.endContainer === this) {
// selection ends in the current text node
// update the end container to be the last node
rangeObject.endContainer = prevNode;
// update the end offset
rangeObject.endOffset += prevNode.nodeValue.length;
// set the flag for range modification
modifiedRange = true;
} else if (rangeObject.endContainer === prevNode.parentNode
&& rangeObject.endOffset === that.getIndexInParent(prevNode) + 1) {
// selection ends right between the previous and current text nodes (which will be merged)
// update the end container to the previous node
rangeObject.endContainer = prevNode;
// set the end offset
rangeObject.endOffset = prevNode.nodeValue.length;
// set the flag for range modification
modifiedRange = true;
}
// now append the contents of the current text node into the previous
prevNode.data += this.data;
// remove empty text nodes
} else if ( this.nodeValue === '' && cleanup.removeempty ) {
// do nothing here.
// remember it as the last text node if not empty
} else if ( !(this.nodeValue === '' && cleanup.removeempty) ) {
prevNode = this;
// we are finish here don't delete this node
break;
}
// now we check whether the selection starts or ends in the mother node of this
if (rangeObject.startContainer === this.parentNode && rangeObject.startOffset > index) {
// there will be one less object, so reduce the startOffset by one
rangeObject.startOffset = rangeObject.startOffset - 1;
// set the flag for range modification
modifiedRange = true;
}
if (rangeObject.endContainer === this.parentNode && rangeObject.endOffset > index) {
// there will be one less object, so reduce the endOffset by one
rangeObject.endOffset = rangeObject.endOffset - 1;
// set the flag for range modification
modifiedRange = true;
}
// remove this text node
jQuery(this).remove();
break;
}
});
// eventually remove the startnode itself
// if (cleanup.removeempty
// && GENTICS.Utils.Dom.isBlockLevelElement(start)
// && (!start.childNodes || start.childNodes.length === 0)) {
// if (rangeObject.startContainer == start) {
// rangeObject.startContainer = start.parentNode;
// rangeObject.startOffset = GENTICS.Utils.Dom.getIndexInParent(start);
// }
// if (rangeObject.endContainer == start) {
// rangeObject.endContainer = start.parentNode;
// rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(start);
// }
// startObject.remove();
// modifiedRange = true;
// }
if (modifiedRange) {
rangeObject.clearCaches();
}
return modifiedRange;
},
/**
* Get the index of the given node within its parent node
* @param {DOMObject} node node to check
* @return {Integer} index in the parent node or false if no node given or node has no parent
* @method
*/
getIndexInParent: function (node) {
if (!node) {
return false;
}
var
index = 0,
check = node.previousSibling;
while(check) {
index++;
check = check.previousSibling;
}
return index;
},
/**
* Check whether the given node is a blocklevel element
* @param {DOMObject} node node to check
* @return {boolean} true if yes, false if not (or null)
* @method
*/
isBlockLevelElement: function (node) {
if (!node) {
return false;
}
if (node.nodeType === 1 && jQuery.inArray(node.nodeName.toLowerCase(), this.blockLevelElements) >= 0) {
return true;
} else {
return false;
}
},
/**
* Check whether the given node is a linebreak element
* @param {DOMObject} node node to check
* @return {boolean} true for linebreak elements, false for everything else
* @method
*/
isLineBreakElement: function (node) {
if (!node) {
return false;
}
return node.nodeType === 1 && node.nodeName.toLowerCase() == 'br';
},
/**
* Check whether the given node is a list element
* @param {DOMObject} node node to check
* @return {boolean} true for list elements (li, ul, ol), false for everything else
* @method
*/
isListElement: function (node) {
if (!node) {
return false;
}
return node.nodeType === 1 && jQuery.inArray(node.nodeName.toLowerCase(), this.listElements) >= 0;
},
/**
* This method checks, whether the passed dom object is a dom object, that would
* be split in cases of pressing enter. This currently is true for paragraphs
* and headings
* @param {DOMObject} el
* dom object to check
* @return {boolean} true for split objects, false for other
* @method
*/
isSplitObject: function(el) {
if (el.nodeType === 1){
switch(el.nodeName.toLowerCase()) {
case 'p':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
case 'li':
return true;
}
}
return false;
},
/**
* Starting with the given position (between nodes), search in the given direction to an adjacent notempty text node
* @param {DOMObject} parent parent node containing the position
* @param {Integer} index index of the position within the parent node
* @param {boolean} searchleft true when search direction is 'left' (default), false for 'right'
* @param {object} stopat define at which types of element we shall stop, may contain the following properties
* * - blocklevel (default: true) * - list (default: true) * - linebreak (default: true) ** @return {DOMObject} the found text node or false if none found * @method */ searchAdjacentTextNode: function (parent, index, searchleft, stopat) { if (!parent || parent.nodeType !== 1 || index < 0 || index > parent.childNodes.length) { return false; } if (typeof stopat === 'undefined') { stopat = {'blocklevel' : true, 'list' : true, 'linebreak' : true}; } if (typeof stopat.blocklevel === 'undefined') { stopat.blocklevel = true; } if (typeof stopat.list === 'undefined') { stopat.list = true; } if (typeof stopat.linebreak === 'undefined') { stopat.linebreak = true; } if (typeof searchleft === 'undefined') { searchleft = true; } var nextNode, currentParent = parent; // start at the node left/right of the given position if (searchleft && index > 0) { nextNode = parent.childNodes[index - 1]; } if (!searchleft && index < parent.childNodes.length) { nextNode = parent.childNodes[index]; } //currentParent is not a number therefore it is sufficient to directly test for it with while(currentParent) //otherwise there would be an error if the object is null while (currentParent) { //while (typeof currentParent !== 'undefined') { if (!nextNode) { // no next node found, check whether the parent is a blocklevel element if (stopat.blocklevel && this.isBlockLevelElement(currentParent)) { // do not leave block level elements return false; } else if (stopat.list && this.isListElement(currentParent)) { // do not leave list elements return false; } else { // continue with the parent nextNode = searchleft ? currentParent.previousSibling : currentParent.nextSibling; currentParent = currentParent.parentNode; } } else if (nextNode.nodeType === 3 && jQuery.trim(nextNode.data).length > 0) { // we are lucky and found a notempty text node return nextNode; } else if (stopat.blocklevel && this.isBlockLevelElement(nextNode)) { // we found a blocklevel element, stop here return false; } else if (stopat.linebreak && this.isLineBreakElement(nextNode)) { // we found a linebreak, stop here return false; } else if (stopat.list && this.isListElement(nextNode)) { // we found a linebreak, stop here return false; } else if (nextNode.nodeType === 3) { // we found an empty text node, so step to the next nextNode = searchleft ? nextNode.previousSibling : nextNode.nextSibling; } else { // we found a non-blocklevel element, step into currentParent = nextNode; nextNode = searchleft ? nextNode.lastChild : nextNode.firstChild; } } }, /** * Insert the given DOM Object into the start/end of the given range. The method * will find the appropriate place in the DOM tree for inserting the given * object, and will eventually split elements in between. The given range will * be updated if necessary. The updated range will NOT embrace the inserted * object, which means that the object is actually inserted before or after the * given range (depending on the atEnd parameter) * * @param {jQuery} * object object to insert into the DOM * @param {GENTICS.Utils.RangeObject} * range range where to insert the object (at start or end) * @param {jQuery} * limit limiting object(s) of the DOM modification * @param {boolean} * atEnd true when the object shall be inserted at the end, false for * insertion at the start (default) * @param {boolean} * true when the insertion shall be done, even if inserting the element * would not be allowed, false to deny inserting unallowed elements (default) * @return true if the object could be inserted, false if not. * @method */ insertIntoDOM: function (object, range, limit, atEnd, force) { // first find the appropriate place to insert the given object var parentElements = range.getContainerParents(limit, atEnd), that = this, newParent, container, offset, splitParts, contents; if (!limit) { limit = jQuery(document.body); } // if no parent elements exist (up to the limit), the new parent will be the // limiter itself if (parentElements.length === 0) { newParent = limit.get(0); } else { jQuery.each(parentElements, function (index, parent) { if (that.allowsNesting(parent, object.get(0))) { newParent = parent; return false; } }); } if (typeof newParent === 'undefined' && limit.length > 0) { // found no possible new parent, so split up to the limit object newParent = limit.get(0); } // check whether it is allowed to insert the element at all if (!this.allowsNesting(newParent, object.get(0)) && !force) { return false; } if (typeof newParent !== 'undefined') { // we found a possible new parent, so we split the DOM up to the new parent splitParts = this.split(range, jQuery(newParent), atEnd); if (splitParts === true) { // DOM was not split (there was no need to split it), insert the new object anyway container = range.startContainer; offset = range.startOffset; if (atEnd) { container = range.endContainer; offset = range.endOffset; } if (offset === 0) { // insert right before the first element in the container contents = jQuery(container).contents(); if (contents.length > 0) { contents.eq(0).before(object); } else { jQuery(container).append(object); } return true; } else { // insert right after the element at offset-1 jQuery(container).contents().eq(offset-1).after(object); return true; } } else if (splitParts) { // if the DOM could be split, we insert the new object in between the split parts splitParts.eq(0).after(object); return true; } else { // could not split, so could not insert return false; } } else { // found no possible new parent, so we shall not insert return false; } }, /** * Remove the given DOM object from the DOM and modify the given range to reflect the user expected range after the object was removed * TODO: finish this * @param {DOMObject} object DOM object to remove * @param {GENTICS.Utils.RangeObject} range range which eventually be modified * @param {boolean} preserveContent true if the contents of the removed DOM object shall be preserved, false if not (default: false) * @return true if the DOM object could be removed, false if not * @hide */ removeFromDOM: function (object, range, preserveContent) { if (preserveContent) { // check whether the range will need modification var indexInParent = this.getIndexInParent(object), numChildren = jQuery(object).contents().length, parent = object.parentNode; if (range.startContainer == parent && range.startOffset > indexInParent) { range.startOffset += numChildren - 1; } else if (range.startContainer == object) { range.startContainer = parent; range.startOffset = indexInParent + range.startOffset; } if (range.endContainer == parent && range.endOffset > indexInParent) { range.endOffset += numChildren - 1; } else if (range.endContainer == object) { range.endContainer = parent; range.endOffset = indexInParent + range.endOffset; } // we simply unwrap the children of the object jQuery(object).contents().unwrap(); // optionally do cleanup this.doCleanup({'merge' : true}, range, parent); } else { // TODO } }, /** * Remove the content defined by the given range from the DOM. Update the given * range object to be a collapsed selection at the place of the previous * selection. * @param rangeObject range object * @return true if the range could be removed, false if not */ removeRange: function (rangeObject) { if (!rangeObject) { // no range given return false; } if (rangeObject.isCollapsed()) { // the range is collapsed, nothing to delete return false; } // split partially contained text nodes at the start and end of the range if (rangeObject.startContainer.nodeType == 3 && rangeObject.startOffset > 0 && rangeObject.startOffset < rangeObject.startContainer.data.length) { this.split(rangeObject, jQuery(rangeObject.startContainer).parent(), false); } if (rangeObject.endContainer.nodeType == 3 && rangeObject.endOffset > 0 && rangeObject.endOffset < rangeObject.endContainer.data.length) { this.split(rangeObject, jQuery(rangeObject.endContainer).parent(), true); } // construct the range tree var rangeTree = rangeObject.getRangeTree(); // collapse the range rangeObject.endContainer = rangeObject.startContainer; rangeObject.endOffset = rangeObject.startOffset; // remove the markup from the range tree this.recursiveRemoveRange(rangeTree, rangeObject); // do some cleanup this.doCleanup({'merge' : true}, rangeObject); // this.doCleanup({'merge' : true, 'removeempty' : true}, rangeObject); // clear the caches of the range object rangeObject.clearCaches(); }, recursiveRemoveRange: function (rangeTree, rangeObject) { // iterate over the rangetree objects of this level for (var i = 0; i < rangeTree.length; ++i) { // check for nodes fully in the range if (rangeTree[i].type == 'full') { // if the domobj is the startcontainer, or the startcontainer is inside the domobj, we need to update the rangeObject if (jQuery(rangeObject.startContainer).parents().andSelf().filter(rangeTree[i].domobj).length > 0) { rangeObject.startContainer = rangeObject.endContainer = rangeTree[i].domobj.parentNode; rangeObject.startOffset = rangeObject.endOffset = this.getIndexInParent(rangeTree[i].domobj); } // remove the object from the DOM jQuery(rangeTree[i].domobj).remove(); } else if (rangeTree[i].type == 'partial' && rangeTree[i].children) { // node partially selected and has children, so do recursion this.recursiveRemoveRange(rangeTree[i].children, rangeObject); } } }, /** * Extend the given range to have start and end at the nearest word boundaries to the left (start) and right (end) * @param {GENTICS.Utils.RangeObject} range range to be extended * @param {boolean} fromBoundaries true if extending will also be done, if one or both ends of the range already are at a word boundary, false if not, default: false * @method */ extendToWord: function (range, fromBoundaries) { // search the word boundaries to the left and right var leftBoundary = this.searchWordBoundary(range.startContainer, range.startOffset, true), rightBoundary = this.searchWordBoundary(range.endContainer, range.endOffset, false); // check whether we must not extend the range from word boundaries if (!fromBoundaries) { // we only extend the range if both ends would be different if (range.startContainer == leftBoundary.container && range.startOffset == leftBoundary.offset) { return; } if (range.endContainer == rightBoundary.container && range.endOffset == rightBoundary.offset) { return; } } // set the new boundaries range.startContainer = leftBoundary.container; range.startOffset = leftBoundary.offset; range.endContainer = rightBoundary.container; range.endOffset = rightBoundary.offset; // correct the range range.correctRange(); // clear caches range.clearCaches(); }, /** * Helper method to check whether the given DOM object is a word boundary. * @param {DOMObject} object DOM object in question * @return {boolean} true when the DOM object is a word boundary, false if not * @hide */ isWordBoundaryElement: function (object) { if (!object || !object.nodeName) { return false; } return jQuery.inArray(object.nodeName.toLowerCase(), this.nonWordBoundaryTags) == -1; }, /** * Search for the next word boundary, starting at the given position * @param {DOMObject} container container of the start position * @param {Integer} offset offset of the start position * @param {boolean} searchleft true for searching to the left, false for searching to the right (default: true) * @return {object} object with properties 'container' and 'offset' marking the found word boundary * @method */ searchWordBoundary: function (container, offset, searchleft) { if (typeof searchleft === 'undefined') { searchleft = true; } var boundaryFound = false, wordBoundaryPos, tempWordBoundaryPos, textNode; while (!boundaryFound) { // check the node type if (container.nodeType === 3) { // we are currently in a text node // find the nearest word boundary character if (!searchleft) { // search right wordBoundaryPos = container.data.substring(offset).search(this.nonWordRegex); if (wordBoundaryPos != -1) { // found a word boundary offset = offset + wordBoundaryPos; boundaryFound = true; } else { // found no word boundary, so we set the position after the container offset = this.getIndexInParent(container) + 1; container = container.parentNode; } } else { // search left wordBoundaryPos = container.data.substring(0, offset).search(this.nonWordRegex); tempWordBoundaryPos = wordBoundaryPos; while (tempWordBoundaryPos != -1) { wordBoundaryPos = tempWordBoundaryPos; tempWordBoundaryPos = container.data.substring( wordBoundaryPos + 1, offset).search(this.nonWordRegex); if (tempWordBoundaryPos != -1) { tempWordBoundaryPos = tempWordBoundaryPos + wordBoundaryPos + 1; } } if (wordBoundaryPos != -1) { // found a word boundary offset = wordBoundaryPos + 1; boundaryFound = true; } else { // found no word boundary, so we set the position before the container offset = this.getIndexInParent(container); container = container.parentNode; } } } else if (container.nodeType === 1) { // we are currently in an element node (between nodes) if (!searchleft) { // check whether there is an element to the right if (offset < container.childNodes.length) { // there is an element to the right, check whether it is a word boundary element if (this.isWordBoundaryElement(container.childNodes[offset])) { // we are done boundaryFound = true; } else { // element to the right is no word boundary, so enter it container = container.childNodes[offset]; offset = 0; } } else { // no element to the right, check whether the element itself is a boundary element if (this.isWordBoundaryElement(container)) { // we are done boundaryFound = true; } else { // element itself is no boundary element, so go to parent offset = this.getIndexInParent(container) + 1; container = container.parentNode; } } } else { // check whether there is an element to the left if (offset > 0) { // there is an element to the left, check whether it is a word boundary element if (this.isWordBoundaryElement(container.childNodes[offset - 1])) { // we are done boundaryFound = true; } else { // element to the left is no word boundary, so enter it container = container.childNodes[offset - 1]; offset = container.nodeType === 3 ? container.data.length : container.childNodes.length; } } else { // no element to the left, check whether the element itself is a boundary element if (this.isWordBoundaryElement(container)) { // we are done boundaryFound = true; } else { // element itself is no boundary element, so go to parent offset = this.getIndexInParent(container); container = container.parentNode; } } } } } if (container.nodeType !== 3) { textNode = this.searchAdjacentTextNode(container, offset, !searchleft); if (textNode) { container = textNode; offset = searchleft ? 0 : container.data.length; } } return {'container' : container, 'offset' : offset}; }, /** * Check whether the given dom object is empty * @param {DOMObject} domObject object to check * @return {boolean} true when the object is empty, false if not * @method */ isEmpty: function (domObject) { // a non dom object is considered empty if (!domObject) { return true; } // some tags are considered to be non-empty if (jQuery.inArray(domObject.nodeName.toLowerCase(), this.nonEmptyTags) != -1) { return false; } // text nodes are not empty, if they contain non-whitespace characters if (domObject.nodeType === 3) { return domObject.data.search(/\S/) == -1; } // all other nodes are not empty if they contain at least one child which is not empty for (var i = 0, childNodes = domObject.childNodes.length; i < childNodes; ++i) { if (!this.isEmpty(domObject.childNodes[i])) { return false; } } // found no contents, so the element is empty return true; }, /** * Set the cursor (collapsed selection) right after the given DOM object * @param domObject DOM object * @method */ setCursorAfter: function (domObject) { var newRange = new GENTICS.Utils.RangeObject(), index = this.getIndexInParent(domObject), targetNode, offset; // selection cannot be set between to TEXT_NODEs // if domOject is a Text node set selection at last position in that node if ( domObject.nodeType == 3) { targetNode = domObject; offset = targetNode.nodeValue.length; // if domOject is a Text node set selection at last position in that node } else if ( domObject.nextSibling && domObject.nextSibling.nodeType == 3) { targetNode = domObject.nextSibling; offset = 0; } else { targetNode = domObject.parentNode; offset = this.getIndexInParent(domObject) + 1; } newRange.startContainer = newRange.endContainer = targetNode; newRange.startOffset = newRange.endOffset = offset; // select the range newRange.select(); return newRange; }, /** * Select a DOM node * will create a new range which spans the provided dom node and selects it afterwards * @param domObject DOM object * @method */ selectDomNode: function (domObject) { var newRange = new GENTICS.Utils.RangeObject(); newRange.startContainer = newRange.endContainer = domObject.parentNode; newRange.startOffset = this.getIndexInParent(domObject); newRange.endOffset = newRange.startOffset + 1; newRange.select(); }, /** * Set the cursor (collapsed selection) at the start into the given DOM object * @param domObject DOM object * @method */ setCursorInto: function (domObject) { // set a new range into the given dom object var newRange = new GENTICS.Utils.RangeObject(); newRange.startContainer = newRange.endContainer = domObject; newRange.startOffset = newRange.endOffset = 0; // select the range newRange.select(); }, /** * "An editing host is a node that is either an Element with a contenteditable * attribute set to the true state, or the Element child of a Document whose * designMode is enabled." * @param domObject DOM object * @method */ isEditingHost: function (node) { return node && node.nodeType == 1 //ELEMENT_NODE && (node.contentEditable == "true" || (node.parentNode && node.parentNode.nodeType == 9 //DOCUEMENT_NODE && node.parentNode.designMode == "on")); }, /** * "Something is editable if it is a node which is not an editing host, does * not have a contenteditable attribute set to the false state, and whose * parent is an editing host or editable." * @param domObject DOM object * @method */ isEditable: function (node) { // This is slightly a lie, because we're excluding non-HTML elements with // contentEditable attributes. return node && !this.isEditingHost(node) && (node.nodeType != 1 || node.contentEditable != "false") // ELEMENT_NODE && (this.isEditingHost(node.parentNode) || this.isEditable(node.parentNode)); }, /** * "The editing host of node is null if node is neither editable nor an editing * host; node itself, if node is an editing host; or the nearest ancestor of * node that is an editing host, if node is editable." * @param domObject DOM object * @method */ getEditingHostOf: function(node) { if (this.isEditingHost(node)) { return node; } else if (this.isEditable(node)) { var ancestor = node.parentNode; while (!this.isEditingHost(ancestor)) { ancestor = ancestor.parentNode; } return ancestor; } else { return null; } }, /** * * "Two nodes are in the same editing host if the editing host of the first is * non-null and the same as the editing host of the second." * @param node1 DOM object * @param node2 DOM object * @method */ inSameEditingHost: function (node1, node2) { return this.getEditingHostOf(node1) && this.getEditingHostOf(node1) == this.getEditingHostOf(node2); }, // "A block node is either an Element whose "display" property does not have // resolved value "inline" or "inline-block" or "inline-table" or "none", or a // Document, or a DocumentFragment." isBlockNode: function (node) { return node && ((node.nodeType == $_.Node.ELEMENT_NODE && $_( ["inline", "inline-block", "inline-table", "none"] ).indexOf($_.getComputedStyle(node).display) == -1) || node.nodeType == $_.Node.DOCUMENT_NODE || node.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE); }, /** * Get the first visible child of the given node. * @param node node * @param includeNode when set to true, the node itself may be returned, otherwise only children are allowed * @return first visible child or null if none found */ getFirstVisibleChild: function (node, includeNode) { // no node -> no child if (!node) { return null; } // check whether the node itself is visible if ((node.nodeType == $_.Node.TEXT_NODE && this.isEmpty(node)) || (node.nodeType == $_.Node.ELEMENT_NODE && node.offsetHeight == 0 && jQuery.inArray(node.nodeName.toLowerCase(), this.nonEmptyTags) === -1)) { return null; } // if the node is a text node, or does not have children, or is not editable, it is the first visible child if (node.nodeType == $_.Node.TEXT_NODE || (node.nodeType == $_.Node.ELEMENT_NODE && node.childNodes.length == 0) || !jQuery(node).contentEditable()) { return includeNode ? node : null; } // otherwise traverse through the children for (var i = 0; i < node.childNodes.length; ++i) { var visibleChild = this.getFirstVisibleChild(node.childNodes[i], true); if (visibleChild != null) { return visibleChild; } } return null; }, /** * Get the last visible child of the given node. * @param node node * @param includeNode when set to true, the node itself may be returned, otherwise only children are allowed * @return last visible child or null if none found */ getLastVisibleChild: function (node, includeNode) { // no node -> no child if (!node) { return null; } // check whether the node itself is visible if ((node.nodeType == $_.Node.TEXT_NODE && this.isEmpty(node)) || (node.nodeType == $_.Node.ELEMENT_NODE && node.offsetHeight == 0 && jQuery.inArray(node.nodeName.toLowerCase(), this.nonEmptyTags) === -1)) { return null; } // if the node is a text node, or does not have children, or is not editable, it is the first visible child if (node.nodeType == $_.Node.TEXT_NODE || (node.nodeType == $_.Node.ELEMENT_NODE && node.childNodes.length == 0) || !jQuery(node).contentEditable()) { return includeNode ? node : null; } // otherwise traverse through the children for (var i = node.childNodes.length - 1; i >= 0; --i) { var visibleChild = this.getLastVisibleChild(node.childNodes[i], true); if (visibleChild != null) { return visibleChild; } } return null; } }); /** * Create the singleton object * @hide */ GENTICS.Utils.Dom = new Dom(); return GENTICS.Utils.Dom; });