src/pyams_skin/resources/js/ext/tinymce/dev/classes/dom/RangeUtils.js
changeset 69 a361355b55c7
equal deleted inserted replaced
68:fd8fb93e1b6a 69:a361355b55c7
       
     1 /**
       
     2  * RangeUtils.js
       
     3  *
       
     4  * Copyright, Moxiecode Systems AB
       
     5  * Released under LGPL License.
       
     6  *
       
     7  * License: http://www.tinymce.com/license
       
     8  * Contributing: http://www.tinymce.com/contributing
       
     9  */
       
    10 
       
    11 /**
       
    12  * This class contains a few utility methods for ranges.
       
    13  *
       
    14  * @class tinymce.dom.RangeUtils
       
    15  */
       
    16 define("tinymce/dom/RangeUtils", [
       
    17 	"tinymce/util/Tools",
       
    18 	"tinymce/dom/TreeWalker"
       
    19 ], function(Tools, TreeWalker) {
       
    20 	var each = Tools.each;
       
    21 
       
    22 	function getEndChild(container, index) {
       
    23 		var childNodes = container.childNodes;
       
    24 
       
    25 		index--;
       
    26 
       
    27 		if (index > childNodes.length - 1) {
       
    28 			index = childNodes.length - 1;
       
    29 		} else if (index < 0) {
       
    30 			index = 0;
       
    31 		}
       
    32 
       
    33 		return childNodes[index] || container;
       
    34 	}
       
    35 
       
    36 	function RangeUtils(dom) {
       
    37 		/**
       
    38 		 * Walks the specified range like object and executes the callback for each sibling collection it finds.
       
    39 		 *
       
    40 		 * @method walk
       
    41 		 * @param {Object} rng Range like object.
       
    42 		 * @param {function} callback Callback function to execute for each sibling collection.
       
    43 		 */
       
    44 		this.walk = function(rng, callback) {
       
    45 			var startContainer = rng.startContainer,
       
    46 				startOffset = rng.startOffset,
       
    47 				endContainer = rng.endContainer,
       
    48 				endOffset = rng.endOffset,
       
    49 				ancestor, startPoint,
       
    50 				endPoint, node, parent, siblings, nodes;
       
    51 
       
    52 			// Handle table cell selection the table plugin enables
       
    53 			// you to fake select table cells and perform formatting actions on them
       
    54 			nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
       
    55 			if (nodes.length > 0) {
       
    56 				each(nodes, function(node) {
       
    57 					callback([node]);
       
    58 				});
       
    59 
       
    60 				return;
       
    61 			}
       
    62 
       
    63 			/**
       
    64 			 * Excludes start/end text node if they are out side the range
       
    65 			 *
       
    66 			 * @private
       
    67 			 * @param {Array} nodes Nodes to exclude items from.
       
    68 			 * @return {Array} Array with nodes excluding the start/end container if needed.
       
    69 			 */
       
    70 			function exclude(nodes) {
       
    71 				var node;
       
    72 
       
    73 				// First node is excluded
       
    74 				node = nodes[0];
       
    75 				if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
       
    76 					nodes.splice(0, 1);
       
    77 				}
       
    78 
       
    79 				// Last node is excluded
       
    80 				node = nodes[nodes.length - 1];
       
    81 				if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
       
    82 					nodes.splice(nodes.length - 1, 1);
       
    83 				}
       
    84 
       
    85 				return nodes;
       
    86 			}
       
    87 
       
    88 			/**
       
    89 			 * Collects siblings
       
    90 			 *
       
    91 			 * @private
       
    92 			 * @param {Node} node Node to collect siblings from.
       
    93 			 * @param {String} name Name of the sibling to check for.
       
    94 			 * @return {Array} Array of collected siblings.
       
    95 			 */
       
    96 			function collectSiblings(node, name, end_node) {
       
    97 				var siblings = [];
       
    98 
       
    99 				for (; node && node != end_node; node = node[name]) {
       
   100 					siblings.push(node);
       
   101 				}
       
   102 
       
   103 				return siblings;
       
   104 			}
       
   105 
       
   106 			/**
       
   107 			 * Find an end point this is the node just before the common ancestor root.
       
   108 			 *
       
   109 			 * @private
       
   110 			 * @param {Node} node Node to start at.
       
   111 			 * @param {Node} root Root/ancestor element to stop just before.
       
   112 			 * @return {Node} Node just before the root element.
       
   113 			 */
       
   114 			function findEndPoint(node, root) {
       
   115 				do {
       
   116 					if (node.parentNode == root) {
       
   117 						return node;
       
   118 					}
       
   119 
       
   120 					node = node.parentNode;
       
   121 				} while (node);
       
   122 			}
       
   123 
       
   124 			function walkBoundary(start_node, end_node, next) {
       
   125 				var siblingName = next ? 'nextSibling' : 'previousSibling';
       
   126 
       
   127 				for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
       
   128 					parent = node.parentNode;
       
   129 					siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
       
   130 
       
   131 					if (siblings.length) {
       
   132 						if (!next) {
       
   133 							siblings.reverse();
       
   134 						}
       
   135 
       
   136 						callback(exclude(siblings));
       
   137 					}
       
   138 				}
       
   139 			}
       
   140 
       
   141 			// If index based start position then resolve it
       
   142 			if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
       
   143 				startContainer = startContainer.childNodes[startOffset];
       
   144 			}
       
   145 
       
   146 			// If index based end position then resolve it
       
   147 			if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
       
   148 				endContainer = getEndChild(endContainer, endOffset);
       
   149 			}
       
   150 
       
   151 			// Same container
       
   152 			if (startContainer == endContainer) {
       
   153 				return callback(exclude([startContainer]));
       
   154 			}
       
   155 
       
   156 			// Find common ancestor and end points
       
   157 			ancestor = dom.findCommonAncestor(startContainer, endContainer);
       
   158 
       
   159 			// Process left side
       
   160 			for (node = startContainer; node; node = node.parentNode) {
       
   161 				if (node === endContainer) {
       
   162 					return walkBoundary(startContainer, ancestor, true);
       
   163 				}
       
   164 
       
   165 				if (node === ancestor) {
       
   166 					break;
       
   167 				}
       
   168 			}
       
   169 
       
   170 			// Process right side
       
   171 			for (node = endContainer; node; node = node.parentNode) {
       
   172 				if (node === startContainer) {
       
   173 					return walkBoundary(endContainer, ancestor);
       
   174 				}
       
   175 
       
   176 				if (node === ancestor) {
       
   177 					break;
       
   178 				}
       
   179 			}
       
   180 
       
   181 			// Find start/end point
       
   182 			startPoint = findEndPoint(startContainer, ancestor) || startContainer;
       
   183 			endPoint = findEndPoint(endContainer, ancestor) || endContainer;
       
   184 
       
   185 			// Walk left leaf
       
   186 			walkBoundary(startContainer, startPoint, true);
       
   187 
       
   188 			// Walk the middle from start to end point
       
   189 			siblings = collectSiblings(
       
   190 				startPoint == startContainer ? startPoint : startPoint.nextSibling,
       
   191 				'nextSibling',
       
   192 				endPoint == endContainer ? endPoint.nextSibling : endPoint
       
   193 			);
       
   194 
       
   195 			if (siblings.length) {
       
   196 				callback(exclude(siblings));
       
   197 			}
       
   198 
       
   199 			// Walk right leaf
       
   200 			walkBoundary(endContainer, endPoint);
       
   201 		};
       
   202 
       
   203 		/**
       
   204 		 * Splits the specified range at it's start/end points.
       
   205 		 *
       
   206 		 * @private
       
   207 		 * @param {Range/RangeObject} rng Range to split.
       
   208 		 * @return {Object} Range position object.
       
   209 		 */
       
   210 		this.split = function(rng) {
       
   211 			var startContainer = rng.startContainer,
       
   212 				startOffset = rng.startOffset,
       
   213 				endContainer = rng.endContainer,
       
   214 				endOffset = rng.endOffset;
       
   215 
       
   216 			function splitText(node, offset) {
       
   217 				return node.splitText(offset);
       
   218 			}
       
   219 
       
   220 			// Handle single text node
       
   221 			if (startContainer == endContainer && startContainer.nodeType == 3) {
       
   222 				if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
       
   223 					endContainer = splitText(startContainer, startOffset);
       
   224 					startContainer = endContainer.previousSibling;
       
   225 
       
   226 					if (endOffset > startOffset) {
       
   227 						endOffset = endOffset - startOffset;
       
   228 						startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
       
   229 						endOffset = endContainer.nodeValue.length;
       
   230 						startOffset = 0;
       
   231 					} else {
       
   232 						endOffset = 0;
       
   233 					}
       
   234 				}
       
   235 			} else {
       
   236 				// Split startContainer text node if needed
       
   237 				if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
       
   238 					startContainer = splitText(startContainer, startOffset);
       
   239 					startOffset = 0;
       
   240 				}
       
   241 
       
   242 				// Split endContainer text node if needed
       
   243 				if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
       
   244 					endContainer = splitText(endContainer, endOffset).previousSibling;
       
   245 					endOffset = endContainer.nodeValue.length;
       
   246 				}
       
   247 			}
       
   248 
       
   249 			return {
       
   250 				startContainer: startContainer,
       
   251 				startOffset: startOffset,
       
   252 				endContainer: endContainer,
       
   253 				endOffset: endOffset
       
   254 			};
       
   255 		};
       
   256 
       
   257 		/**
       
   258 		 * Normalizes the specified range by finding the closest best suitable caret location.
       
   259 		 *
       
   260 		 * @private
       
   261 		 * @param {Range} rng Range to normalize.
       
   262 		 * @return {Boolean} True/false if the specified range was normalized or not.
       
   263 		 */
       
   264 		this.normalize = function(rng) {
       
   265 			var normalized, collapsed;
       
   266 
       
   267 			function normalizeEndPoint(start) {
       
   268 				var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
       
   269 				var directionLeft, isAfterNode;
       
   270 
       
   271 				function hasBrBeforeAfter(node, left) {
       
   272 					var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
       
   273 
       
   274 					while ((node = walker[left ? 'prev' : 'next']())) {
       
   275 						if (node.nodeName === "BR") {
       
   276 							return true;
       
   277 						}
       
   278 					}
       
   279 				}
       
   280 
       
   281 				function isPrevNode(node, name) {
       
   282 					return node.previousSibling && node.previousSibling.nodeName == name;
       
   283 				}
       
   284 
       
   285 				// Walks the dom left/right to find a suitable text node to move the endpoint into
       
   286 				// It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
       
   287 				function findTextNodeRelative(left, startNode) {
       
   288 					var walker, lastInlineElement, parentBlockContainer;
       
   289 
       
   290 					startNode = startNode || container;
       
   291 					parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
       
   292 
       
   293 					// Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
       
   294 					// This: <p><br>|</p> becomes <p>|<br></p>
       
   295 					if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
       
   296 						container = startNode.parentNode;
       
   297 						offset = dom.nodeIndex(startNode);
       
   298 						normalized = true;
       
   299 						return;
       
   300 					}
       
   301 
       
   302 					// Walk left until we hit a text node we can move to or a block/br/img
       
   303 					walker = new TreeWalker(startNode, parentBlockContainer);
       
   304 					while ((node = walker[left ? 'prev' : 'next']())) {
       
   305 						// Break if we hit a non content editable node
       
   306 						if (dom.getContentEditableParent(node) === "false") {
       
   307 							return;
       
   308 						}
       
   309 
       
   310 						// Found text node that has a length
       
   311 						if (node.nodeType === 3 && node.nodeValue.length > 0) {
       
   312 							container = node;
       
   313 							offset = left ? node.nodeValue.length : 0;
       
   314 							normalized = true;
       
   315 							return;
       
   316 						}
       
   317 
       
   318 						// Break if we find a block or a BR/IMG/INPUT etc
       
   319 						if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
       
   320 							return;
       
   321 						}
       
   322 
       
   323 						lastInlineElement = node;
       
   324 					}
       
   325 
       
   326 					// Only fetch the last inline element when in caret mode for now
       
   327 					if (collapsed && lastInlineElement) {
       
   328 						container = lastInlineElement;
       
   329 						normalized = true;
       
   330 						offset = 0;
       
   331 					}
       
   332 				}
       
   333 
       
   334 				container = rng[(start ? 'start' : 'end') + 'Container'];
       
   335 				offset = rng[(start ? 'start' : 'end') + 'Offset'];
       
   336 				isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
       
   337 				nonEmptyElementsMap = dom.schema.getNonEmptyElements();
       
   338 				directionLeft = start;
       
   339 
       
   340 				if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
       
   341 					directionLeft = false;
       
   342 				}
       
   343 
       
   344 				// If the container is a document move it to the body element
       
   345 				if (container.nodeType === 9) {
       
   346 					container = dom.getRoot();
       
   347 					offset = 0;
       
   348 				}
       
   349 
       
   350 				// If the container is body try move it into the closest text node or position
       
   351 				if (container === body) {
       
   352 					// If start is before/after a image, table etc
       
   353 					if (directionLeft) {
       
   354 						node = container.childNodes[offset > 0 ? offset - 1 : 0];
       
   355 						if (node) {
       
   356 							if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
       
   357 								return;
       
   358 							}
       
   359 						}
       
   360 					}
       
   361 
       
   362 					// Resolve the index
       
   363 					if (container.hasChildNodes()) {
       
   364 						offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
       
   365 						container = container.childNodes[offset];
       
   366 						offset = 0;
       
   367 
       
   368 						// Don't walk into elements that doesn't have any child nodes like a IMG
       
   369 						if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
       
   370 							// Walk the DOM to find a text node to place the caret at or a BR
       
   371 							node = container;
       
   372 							walker = new TreeWalker(container, body);
       
   373 
       
   374 							do {
       
   375 								// Found a text node use that position
       
   376 								if (node.nodeType === 3 && node.nodeValue.length > 0) {
       
   377 									offset = directionLeft ? 0 : node.nodeValue.length;
       
   378 									container = node;
       
   379 									normalized = true;
       
   380 									break;
       
   381 								}
       
   382 
       
   383 								// Found a BR/IMG element that we can place the caret before
       
   384 								if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
       
   385 									offset = dom.nodeIndex(node);
       
   386 									container = node.parentNode;
       
   387 
       
   388 									// Put caret after image when moving the end point
       
   389 									if (node.nodeName == "IMG" && !directionLeft) {
       
   390 										offset++;
       
   391 									}
       
   392 
       
   393 									normalized = true;
       
   394 									break;
       
   395 								}
       
   396 							} while ((node = (directionLeft ? walker.next() : walker.prev())));
       
   397 						}
       
   398 					}
       
   399 				}
       
   400 
       
   401 				// Lean the caret to the left if possible
       
   402 				if (collapsed) {
       
   403 					// So this: <b>x</b><i>|x</i>
       
   404 					// Becomes: <b>x|</b><i>x</i>
       
   405 					// Seems that only gecko has issues with this
       
   406 					if (container.nodeType === 3 && offset === 0) {
       
   407 						findTextNodeRelative(true);
       
   408 					}
       
   409 
       
   410 					// Lean left into empty inline elements when the caret is before a BR
       
   411 					// So this: <i><b></b><i>|<br></i>
       
   412 					// Becomes: <i><b>|</b><i><br></i>
       
   413 					// Seems that only gecko has issues with this.
       
   414 					// Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
       
   415 					if (container.nodeType === 1) {
       
   416 						node = container.childNodes[offset];
       
   417 
       
   418 						// Offset is after the containers last child
       
   419 						// then use the previous child for normalization
       
   420 						if (!node) {
       
   421 							node = container.childNodes[offset - 1];
       
   422 						}
       
   423 
       
   424 						if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
       
   425 							!hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
       
   426 							findTextNodeRelative(true, node);
       
   427 						}
       
   428 					}
       
   429 				}
       
   430 
       
   431 				// Lean the start of the selection right if possible
       
   432 				// So this: x[<b>x]</b>
       
   433 				// Becomes: x<b>[x]</b>
       
   434 				if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
       
   435 					findTextNodeRelative(false);
       
   436 				}
       
   437 
       
   438 				// Set endpoint if it was normalized
       
   439 				if (normalized) {
       
   440 					rng['set' + (start ? 'Start' : 'End')](container, offset);
       
   441 				}
       
   442 			}
       
   443 
       
   444 			collapsed = rng.collapsed;
       
   445 
       
   446 			normalizeEndPoint(true);
       
   447 
       
   448 			if (!collapsed) {
       
   449 				normalizeEndPoint();
       
   450 			}
       
   451 
       
   452 			// If it was collapsed then make sure it still is
       
   453 			if (normalized && collapsed) {
       
   454 				rng.collapse(true);
       
   455 			}
       
   456 
       
   457 			return normalized;
       
   458 		};
       
   459 	}
       
   460 
       
   461 	/**
       
   462 	 * Compares two ranges and checks if they are equal.
       
   463 	 *
       
   464 	 * @static
       
   465 	 * @method compareRanges
       
   466 	 * @param {DOMRange} rng1 First range to compare.
       
   467 	 * @param {DOMRange} rng2 First range to compare.
       
   468 	 * @return {Boolean} true/false if the ranges are equal.
       
   469 	 */
       
   470 	RangeUtils.compareRanges = function(rng1, rng2) {
       
   471 		if (rng1 && rng2) {
       
   472 			// Compare native IE ranges
       
   473 			if (rng1.item || rng1.duplicate) {
       
   474 				// Both are control ranges and the selected element matches
       
   475 				if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
       
   476 					return true;
       
   477 				}
       
   478 
       
   479 				// Both are text ranges and the range matches
       
   480 				if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
       
   481 					return true;
       
   482 				}
       
   483 			} else {
       
   484 				// Compare w3c ranges
       
   485 				return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
       
   486 			}
       
   487 		}
       
   488 
       
   489 		return false;
       
   490 	};
       
   491 
       
   492 	/**
       
   493 	 * Gets the caret range for the given x/y location.
       
   494 	 *
       
   495 	 * @static
       
   496 	 * @method getCaretRangeFromPoint
       
   497 	 * @param {Number} x X coordinate for range
       
   498 	 * @param {Number} y Y coordinate for range
       
   499 	 * @param {Document} doc Document that x/y are relative to
       
   500 	 * @returns {Range} caret range
       
   501 	 */
       
   502 	RangeUtils.getCaretRangeFromPoint = function(x, y, doc) {
       
   503 		var rng, point;
       
   504 
       
   505 		if (doc.caretPositionFromPoint) {
       
   506 			point = doc.caretPositionFromPoint(x, y);
       
   507 			rng = doc.createRange();
       
   508 			rng.setStart(point.offsetNode, point.offset);
       
   509 			rng.collapse(true);
       
   510 		} else if (doc.caretRangeFromPoint) {
       
   511 			rng = doc.caretRangeFromPoint(x, y);
       
   512 		} else if (doc.body.createTextRange) {
       
   513 			rng = doc.body.createTextRange();
       
   514 
       
   515 			try {
       
   516 				rng.moveToPoint(x, y);
       
   517 				rng.collapse(true);
       
   518 			} catch (ex) {
       
   519 				// Append to top or bottom depending on drop location
       
   520 				rng.collapse(y < doc.body.clientHeight);
       
   521 			}
       
   522 		}
       
   523 
       
   524 		return rng;
       
   525 	};
       
   526 
       
   527 	RangeUtils.getNode = function(container, offset) {
       
   528 		if (container.nodeType == 1 && container.hasChildNodes()) {
       
   529 			if (offset >= container.childNodes.length) {
       
   530 				offset = container.childNodes.length - 1;
       
   531 			}
       
   532 
       
   533 			container = container.childNodes[offset];
       
   534 		}
       
   535 
       
   536 		return container;
       
   537 	};
       
   538 
       
   539 	return RangeUtils;
       
   540 });