Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 "use strict";
3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
5 var cssStylingFlag = false;
7 var defaultSingleLineContainerName = "p";
9 // This is bad :(
10 var globalRange = null;
12 // Commands are stored in a dictionary where we call their actions and such
13 var commands = {};
15 ///////////////////////////////////////////////////////////////////////////////
16 ////////////////////////////// Utility functions //////////////////////////////
17 ///////////////////////////////////////////////////////////////////////////////
18 //@{
20 function nextNode(node) {
21 if (node.hasChildNodes()) {
22 return node.firstChild;
23 }
24 return nextNodeDescendants(node);
25 }
27 function previousNode(node) {
28 if (node.previousSibling) {
29 node = node.previousSibling;
30 while (node.hasChildNodes()) {
31 node = node.lastChild;
32 }
33 return node;
34 }
35 if (node.parentNode
36 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
37 return node.parentNode;
38 }
39 return null;
40 }
42 function nextNodeDescendants(node) {
43 while (node && !node.nextSibling) {
44 node = node.parentNode;
45 }
46 if (!node) {
47 return null;
48 }
49 return node.nextSibling;
50 }
52 /**
53 * Returns true if ancestor is an ancestor of descendant, false otherwise.
54 */
55 function isAncestor(ancestor, descendant) {
56 return ancestor
57 && descendant
58 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
59 }
61 /**
62 * Returns true if ancestor is an ancestor of or equal to descendant, false
63 * otherwise.
64 */
65 function isAncestorContainer(ancestor, descendant) {
66 return (ancestor || descendant)
67 && (ancestor == descendant || isAncestor(ancestor, descendant));
68 }
70 /**
71 * Returns true if descendant is a descendant of ancestor, false otherwise.
72 */
73 function isDescendant(descendant, ancestor) {
74 return ancestor
75 && descendant
76 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
77 }
79 /**
80 * Returns true if node1 is before node2 in tree order, false otherwise.
81 */
82 function isBefore(node1, node2) {
83 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING);
84 }
86 /**
87 * Returns true if node1 is after node2 in tree order, false otherwise.
88 */
89 function isAfter(node1, node2) {
90 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING);
91 }
93 function getAncestors(node) {
94 var ancestors = [];
95 while (node.parentNode) {
96 ancestors.unshift(node.parentNode);
97 node = node.parentNode;
98 }
99 return ancestors;
100 }
102 function getInclusiveAncestors(node) {
103 return getAncestors(node).concat(node);
104 }
106 function getDescendants(node) {
107 var descendants = [];
108 var stop = nextNodeDescendants(node);
109 while ((node = nextNode(node))
110 && node != stop) {
111 descendants.push(node);
112 }
113 return descendants;
114 }
116 function getInclusiveDescendants(node) {
117 return [node].concat(getDescendants(node));
118 }
120 function convertProperty(property) {
121 // Special-case for now
122 var map = {
123 "fontFamily": "font-family",
124 "fontSize": "font-size",
125 "fontStyle": "font-style",
126 "fontWeight": "font-weight",
127 "textDecoration": "text-decoration",
128 };
129 if (typeof map[property] != "undefined") {
130 return map[property];
131 }
133 return property;
134 }
136 // Return the <font size=X> value for the given CSS size, or undefined if there
137 // is none.
138 function cssSizeToLegacy(cssVal) {
139 return {
140 "x-small": 1,
141 "small": 2,
142 "medium": 3,
143 "large": 4,
144 "x-large": 5,
145 "xx-large": 6,
146 "xxx-large": 7
147 }[cssVal];
148 }
150 // Return the CSS size given a legacy size.
151 function legacySizeToCss(legacyVal) {
152 return {
153 1: "x-small",
154 2: "small",
155 3: "medium",
156 4: "large",
157 5: "x-large",
158 6: "xx-large",
159 7: "xxx-large",
160 }[legacyVal];
161 }
163 // Opera 11 puts HTML elements in the null namespace, it seems.
164 function isHtmlNamespace(ns) {
165 return ns === null
166 || ns === htmlNamespace;
167 }
169 // "the directionality" from HTML. I don't bother caring about non-HTML
170 // elements.
171 //
172 // "The directionality of an element is either 'ltr' or 'rtl', and is
173 // determined as per the first appropriate set of steps from the following
174 // list:"
175 function getDirectionality(element) {
176 // "If the element's dir attribute is in the ltr state
177 // The directionality of the element is 'ltr'."
178 if (element.dir == "ltr") {
179 return "ltr";
180 }
182 // "If the element's dir attribute is in the rtl state
183 // The directionality of the element is 'rtl'."
184 if (element.dir == "rtl") {
185 return "rtl";
186 }
188 // "If the element's dir attribute is in the auto state
189 // "If the element is a bdi element and the dir attribute is not in a
190 // defined state (i.e. it is not present or has an invalid value)
191 // [lots of complicated stuff]
192 //
193 // Skip this, since no browser implements it anyway.
195 // "If the element is a root element and the dir attribute is not in a
196 // defined state (i.e. it is not present or has an invalid value)
197 // The directionality of the element is 'ltr'."
198 if (!isHtmlElement(element.parentNode)) {
199 return "ltr";
200 }
202 // "If the element has a parent element and the dir attribute is not in a
203 // defined state (i.e. it is not present or has an invalid value)
204 // The directionality of the element is the same as the element's
205 // parent element's directionality."
206 return getDirectionality(element.parentNode);
207 }
209 //@}
211 ///////////////////////////////////////////////////////////////////////////////
212 ///////////////////////////// DOM Range functions /////////////////////////////
213 ///////////////////////////////////////////////////////////////////////////////
214 //@{
216 function getNodeIndex(node) {
217 var ret = 0;
218 while (node.previousSibling) {
219 ret++;
220 node = node.previousSibling;
221 }
222 return ret;
223 }
225 // "The length of a Node node is the following, depending on node:
226 //
227 // ProcessingInstruction
228 // DocumentType
229 // Always 0.
230 // Text
231 // Comment
232 // node's length.
233 // Any other node
234 // node's childNodes's length."
235 function getNodeLength(node) {
236 switch (node.nodeType) {
237 case Node.PROCESSING_INSTRUCTION_NODE:
238 case Node.DOCUMENT_TYPE_NODE:
239 return 0;
241 case Node.TEXT_NODE:
242 case Node.COMMENT_NODE:
243 return node.length;
245 default:
246 return node.childNodes.length;
247 }
248 }
250 /**
251 * The position of two boundary points relative to one another, as defined by
252 * DOM Range.
253 */
254 function getPosition(nodeA, offsetA, nodeB, offsetB) {
255 // "If node A is the same as node B, return equal if offset A equals offset
256 // B, before if offset A is less than offset B, and after if offset A is
257 // greater than offset B."
258 if (nodeA == nodeB) {
259 if (offsetA == offsetB) {
260 return "equal";
261 }
262 if (offsetA < offsetB) {
263 return "before";
264 }
265 if (offsetA > offsetB) {
266 return "after";
267 }
268 }
270 // "If node A is after node B in tree order, compute the position of (node
271 // B, offset B) relative to (node A, offset A). If it is before, return
272 // after. If it is after, return before."
273 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
274 var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
275 if (pos == "before") {
276 return "after";
277 }
278 if (pos == "after") {
279 return "before";
280 }
281 }
283 // "If node A is an ancestor of node B:"
284 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
285 // "Let child equal node B."
286 var child = nodeB;
288 // "While child is not a child of node A, set child to its parent."
289 while (child.parentNode != nodeA) {
290 child = child.parentNode;
291 }
293 // "If the index of child is less than offset A, return after."
294 if (getNodeIndex(child) < offsetA) {
295 return "after";
296 }
297 }
299 // "Return before."
300 return "before";
301 }
303 /**
304 * Returns the furthest ancestor of a Node as defined by DOM Range.
305 */
306 function getFurthestAncestor(node) {
307 var root = node;
308 while (root.parentNode != null) {
309 root = root.parentNode;
310 }
311 return root;
312 }
314 /**
315 * "contained" as defined by DOM Range: "A Node node is contained in a range
316 * range if node's furthest ancestor is the same as range's root, and (node, 0)
317 * is after range's start, and (node, length of node) is before range's end."
318 */
319 function isContained(node, range) {
320 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
321 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
323 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer)
324 && pos1 == "after"
325 && pos2 == "before";
326 }
328 /**
329 * Return all nodes contained in range that the provided function returns true
330 * for, omitting any with an ancestor already being returned.
331 */
332 function getContainedNodes(range, condition) {
333 if (typeof condition == "undefined") {
334 condition = function() { return true };
335 }
336 var node = range.startContainer;
337 if (node.hasChildNodes()
338 && range.startOffset < node.childNodes.length) {
339 // A child is contained
340 node = node.childNodes[range.startOffset];
341 } else if (range.startOffset == getNodeLength(node)) {
342 // No descendant can be contained
343 node = nextNodeDescendants(node);
344 } else {
345 // No children; this node at least can't be contained
346 node = nextNode(node);
347 }
349 var stop = range.endContainer;
350 if (stop.hasChildNodes()
351 && range.endOffset < stop.childNodes.length) {
352 // The node after the last contained node is a child
353 stop = stop.childNodes[range.endOffset];
354 } else {
355 // This node and/or some of its children might be contained
356 stop = nextNodeDescendants(stop);
357 }
359 var nodeList = [];
360 while (isBefore(node, stop)) {
361 if (isContained(node, range)
362 && condition(node)) {
363 nodeList.push(node);
364 node = nextNodeDescendants(node);
365 continue;
366 }
367 node = nextNode(node);
368 }
369 return nodeList;
370 }
372 /**
373 * As above, but includes nodes with an ancestor that's already been returned.
374 */
375 function getAllContainedNodes(range, condition) {
376 if (typeof condition == "undefined") {
377 condition = function() { return true };
378 }
379 var node = range.startContainer;
380 if (node.hasChildNodes()
381 && range.startOffset < node.childNodes.length) {
382 // A child is contained
383 node = node.childNodes[range.startOffset];
384 } else if (range.startOffset == getNodeLength(node)) {
385 // No descendant can be contained
386 node = nextNodeDescendants(node);
387 } else {
388 // No children; this node at least can't be contained
389 node = nextNode(node);
390 }
392 var stop = range.endContainer;
393 if (stop.hasChildNodes()
394 && range.endOffset < stop.childNodes.length) {
395 // The node after the last contained node is a child
396 stop = stop.childNodes[range.endOffset];
397 } else {
398 // This node and/or some of its children might be contained
399 stop = nextNodeDescendants(stop);
400 }
402 var nodeList = [];
403 while (isBefore(node, stop)) {
404 if (isContained(node, range)
405 && condition(node)) {
406 nodeList.push(node);
407 }
408 node = nextNode(node);
409 }
410 return nodeList;
411 }
413 // Returns either null, or something of the form rgb(x, y, z), or something of
414 // the form rgb(x, y, z, w) with w != 0.
415 function normalizeColor(color) {
416 if (color.toLowerCase() == "currentcolor") {
417 return null;
418 }
420 if (normalizeColor.resultCache === undefined) {
421 normalizeColor.resultCache = {};
422 }
424 if (normalizeColor.resultCache[color] !== undefined) {
425 return normalizeColor.resultCache[color];
426 }
428 var originalColor = color;
430 var outerSpan = document.createElement("span");
431 document.body.appendChild(outerSpan);
432 outerSpan.style.color = "black";
434 var innerSpan = document.createElement("span");
435 outerSpan.appendChild(innerSpan);
436 innerSpan.style.color = color;
437 color = getComputedStyle(innerSpan).color;
439 if (color == "rgb(0, 0, 0)") {
440 // Maybe it's really black, maybe it's invalid.
441 outerSpan.color = "white";
442 color = getComputedStyle(innerSpan).color;
443 if (color != "rgb(0, 0, 0)") {
444 return normalizeColor.resultCache[originalColor] = null;
445 }
446 }
448 document.body.removeChild(outerSpan);
450 // I rely on the fact that browsers generally provide consistent syntax for
451 // getComputedStyle(), although it's not standardized. There are only
452 // three exceptions I found:
453 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
454 // IE10PP2 seems to do this sometimes.
455 return normalizeColor.resultCache[originalColor] =
456 color.replace("rgba", "rgb").replace(", 1)", ")");
457 }
458 if (color == "transparent") {
459 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
460 // the specified value is "transparent".
461 return normalizeColor.resultCache[originalColor] =
462 "rgba(0, 0, 0, 0)";
463 }
464 // Chrome 15 dev adds way too many significant figures. This isn't a full
465 // fix, it just fixes one case that comes up in tests.
466 color = color.replace(/, 0.496094\)$/, ", 0.5)");
467 return normalizeColor.resultCache[originalColor] = color;
468 }
470 // Returns either null, or something of the form #xxxxxx.
471 function parseSimpleColor(color) {
472 color = normalizeColor(color);
473 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
474 if (matches) {
475 return "#"
476 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
477 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
478 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
479 }
480 return null;
481 }
483 //@}
485 //////////////////////////////////////////////////////////////////////////////
486 /////////////////////////// Edit command functions ///////////////////////////
487 //////////////////////////////////////////////////////////////////////////////
489 /////////////////////////////////////////////////
490 ///// Methods of the HTMLDocument interface /////
491 /////////////////////////////////////////////////
492 //@{
494 var executionStackDepth = 0;
496 // Helper function for common behavior.
497 function editCommandMethod(command, range, callback) {
498 // Set up our global range magic, but only if we're the outermost function
499 if (executionStackDepth == 0 && typeof range != "undefined") {
500 globalRange = range;
501 } else if (executionStackDepth == 0) {
502 globalRange = null;
503 globalRange = getActiveRange();
504 }
506 executionStackDepth++;
507 try {
508 var ret = callback();
509 } catch(e) {
510 executionStackDepth--;
511 throw e;
512 }
513 executionStackDepth--;
514 return ret;
515 }
517 function myExecCommand(command, showUi, value, range) {
518 // "All of these methods must treat their command argument ASCII
519 // case-insensitively."
520 command = command.toLowerCase();
522 // "If only one argument was provided, let show UI be false."
523 //
524 // If range was passed, I can't actually detect how many args were passed
525 // . . .
526 if (arguments.length == 1
527 || (arguments.length >=4 && typeof showUi == "undefined")) {
528 showUi = false;
529 }
531 // "If only one or two arguments were provided, let value be the empty
532 // string."
533 if (arguments.length <= 2
534 || (arguments.length >=4 && typeof value == "undefined")) {
535 value = "";
536 }
538 return editCommandMethod(command, range, (function(command, showUi, value) { return function() {
539 // "If command is not supported or not enabled, return false."
540 if (!(command in commands) || !myQueryCommandEnabled(command)) {
541 return false;
542 }
544 // "Take the action for command, passing value to the instructions as an
545 // argument."
546 var ret = commands[command].action(value);
548 // Check for bugs
549 if (ret !== true && ret !== false) {
550 throw "execCommand() didn't return true or false: " + ret;
551 }
553 // "If the previous step returned false, return false."
554 if (ret === false) {
555 return false;
556 }
558 // "Return true."
559 return true;
560 }})(command, showUi, value));
561 }
563 function myQueryCommandEnabled(command, range) {
564 // "All of these methods must treat their command argument ASCII
565 // case-insensitively."
566 command = command.toLowerCase();
568 return editCommandMethod(command, range, (function(command) { return function() {
569 // "Return true if command is both supported and enabled, false
570 // otherwise."
571 if (!(command in commands)) {
572 return false;
573 }
575 // "Among commands defined in this specification, those listed in
576 // Miscellaneous commands are always enabled, except for the cut
577 // command and the paste command. The other commands defined here are
578 // enabled if the active range is not null, its start node is either
579 // editable or an editing host, its end node is either editable or an
580 // editing host, and there is some editing host that is an inclusive
581 // ancestor of both its start node and its end node."
582 return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss",
583 "usecss"].indexOf(command) != -1
584 || (
585 getActiveRange() !== null
586 && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer))
587 && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer))
588 && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost))
589 );
590 }})(command));
591 }
593 function myQueryCommandIndeterm(command, range) {
594 // "All of these methods must treat their command argument ASCII
595 // case-insensitively."
596 command = command.toLowerCase();
598 return editCommandMethod(command, range, (function(command) { return function() {
599 // "If command is not supported or has no indeterminacy, return false."
600 if (!(command in commands) || !("indeterm" in commands[command])) {
601 return false;
602 }
604 // "Return true if command is indeterminate, otherwise false."
605 return commands[command].indeterm();
606 }})(command));
607 }
609 function myQueryCommandState(command, range) {
610 // "All of these methods must treat their command argument ASCII
611 // case-insensitively."
612 command = command.toLowerCase();
614 return editCommandMethod(command, range, (function(command) { return function() {
615 // "If command is not supported or has no state, return false."
616 if (!(command in commands) || !("state" in commands[command])) {
617 return false;
618 }
620 // "If the state override for command is set, return it."
621 if (typeof getStateOverride(command) != "undefined") {
622 return getStateOverride(command);
623 }
625 // "Return true if command's state is true, otherwise false."
626 return commands[command].state();
627 }})(command));
628 }
630 // "When the queryCommandSupported(command) method on the HTMLDocument
631 // interface is invoked, the user agent must return true if command is
632 // supported, and false otherwise."
633 function myQueryCommandSupported(command) {
634 // "All of these methods must treat their command argument ASCII
635 // case-insensitively."
636 command = command.toLowerCase();
638 return command in commands;
639 }
641 function myQueryCommandValue(command, range) {
642 // "All of these methods must treat their command argument ASCII
643 // case-insensitively."
644 command = command.toLowerCase();
646 return editCommandMethod(command, range, function() {
647 // "If command is not supported or has no value, return the empty string."
648 if (!(command in commands) || !("value" in commands[command])) {
649 return "";
650 }
652 // "If command is "fontSize" and its value override is set, convert the
653 // value override to an integer number of pixels and return the legacy
654 // font size for the result."
655 if (command == "fontsize"
656 && getValueOverride("fontsize") !== undefined) {
657 return getLegacyFontSize(getValueOverride("fontsize"));
658 }
660 // "If the value override for command is set, return it."
661 if (typeof getValueOverride(command) != "undefined") {
662 return getValueOverride(command);
663 }
665 // "Return command's value."
666 return commands[command].value();
667 });
668 }
669 //@}
671 //////////////////////////////
672 ///// Common definitions /////
673 //////////////////////////////
674 //@{
676 // "An HTML element is an Element whose namespace is the HTML namespace."
677 //
678 // I allow an extra argument to more easily check whether something is a
679 // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays
680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul.
681 function isHtmlElement(node, tags) {
682 if (typeof tags == "string") {
683 tags = [tags];
684 }
685 if (typeof tags == "object") {
686 tags = tags.map(function(tag) { return tag.toUpperCase() });
687 }
688 return node
689 && node.nodeType == Node.ELEMENT_NODE
690 && isHtmlNamespace(node.namespaceURI)
691 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1);
692 }
694 // "A prohibited paragraph child name is "address", "article", "aside",
695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
700 // "xmp"."
701 var prohibitedParagraphChildNames = ["address", "article", "aside",
702 "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
703 "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
704 "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
705 "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
706 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
707 "xmp"];
709 // "A prohibited paragraph child is an HTML element whose local name is a
710 // prohibited paragraph child name."
711 function isProhibitedParagraphChild(node) {
712 return isHtmlElement(node, prohibitedParagraphChildNames);
713 }
715 // "A block node is either an Element whose "display" property does not have
716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
717 // Document, or a DocumentFragment."
718 function isBlockNode(node) {
719 return node
720 && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1)
721 || node.nodeType == Node.DOCUMENT_NODE
722 || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE);
723 }
725 // "An inline node is a node that is not a block node."
726 function isInlineNode(node) {
727 return node && !isBlockNode(node);
728 }
730 // "An editing host is a node that is either an HTML element with a
731 // contenteditable attribute set to the true state, or the HTML element child
732 // of a Document whose designMode is enabled."
733 function isEditingHost(node) {
734 return node
735 && isHtmlElement(node)
736 && (node.contentEditable == "true"
737 || (node.parentNode
738 && node.parentNode.nodeType == Node.DOCUMENT_NODE
739 && node.parentNode.designMode == "on"));
740 }
742 // "Something is editable if it is a node; it is not an editing host; it does
743 // not have a contenteditable attribute set to the false state; its parent is
744 // an editing host or editable; and either it is an HTML element, or it is an
745 // svg or math element, or it is not an Element and its parent is an HTML
746 // element."
747 function isEditable(node) {
748 return node
749 && !isEditingHost(node)
750 && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false")
751 && (isEditingHost(node.parentNode) || isEditable(node.parentNode))
752 && (isHtmlElement(node)
753 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg")
754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math")
755 || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)));
756 }
758 // Helper function, not defined in the spec
759 function hasEditableDescendants(node) {
760 for (var i = 0; i < node.childNodes.length; i++) {
761 if (isEditable(node.childNodes[i])
762 || hasEditableDescendants(node.childNodes[i])) {
763 return true;
764 }
765 }
766 return false;
767 }
769 // "The editing host of node is null if node is neither editable nor an editing
770 // host; node itself, if node is an editing host; or the nearest ancestor of
771 // node that is an editing host, if node is editable."
772 function getEditingHostOf(node) {
773 if (isEditingHost(node)) {
774 return node;
775 } else if (isEditable(node)) {
776 var ancestor = node.parentNode;
777 while (!isEditingHost(ancestor)) {
778 ancestor = ancestor.parentNode;
779 }
780 return ancestor;
781 } else {
782 return null;
783 }
784 }
786 // "Two nodes are in the same editing host if the editing host of the first is
787 // non-null and the same as the editing host of the second."
788 function inSameEditingHost(node1, node2) {
789 return getEditingHostOf(node1)
790 && getEditingHostOf(node1) == getEditingHostOf(node2);
791 }
793 // "A collapsed line break is a br that begins a line box which has nothing
794 // else in it, and therefore has zero height."
795 function isCollapsedLineBreak(br) {
796 if (!isHtmlElement(br, "br")) {
797 return false;
798 }
800 // Add a zwsp after it and see if that changes the height of the nearest
801 // non-inline parent. Note: this is not actually reliable, because the
802 // parent might have a fixed height or something.
803 var ref = br.parentNode;
804 while (getComputedStyle(ref).display == "inline") {
805 ref = ref.parentNode;
806 }
807 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
808 ref.style.height = "auto";
809 ref.style.maxHeight = "none";
810 ref.style.minHeight = "0";
811 var space = document.createTextNode("\u200b");
812 var origHeight = ref.offsetHeight;
813 if (origHeight == 0) {
814 throw "isCollapsedLineBreak: original height is zero, bug?";
815 }
816 br.parentNode.insertBefore(space, br.nextSibling);
817 var finalHeight = ref.offsetHeight;
818 space.parentNode.removeChild(space);
819 if (refStyle === null) {
820 // Without the setAttribute() line, removeAttribute() doesn't work in
821 // Chrome 14 dev. I have no idea why.
822 ref.setAttribute("style", "");
823 ref.removeAttribute("style");
824 } else {
825 ref.setAttribute("style", refStyle);
826 }
828 // Allow some leeway in case the zwsp didn't create a whole new line, but
829 // only made an existing line slightly higher. Firefox 6.0a2 shows this
830 // behavior when the first line is bold.
831 return origHeight < finalHeight - 5;
832 }
834 // "An extraneous line break is a br that has no visual effect, in that
835 // removing it from the DOM would not change layout, except that a br that is
836 // the sole child of an li is not extraneous."
837 //
838 // FIXME: This doesn't work in IE, since IE ignores display: none in
839 // contenteditable.
840 function isExtraneousLineBreak(br) {
841 if (!isHtmlElement(br, "br")) {
842 return false;
843 }
845 if (isHtmlElement(br.parentNode, "li")
846 && br.parentNode.childNodes.length == 1) {
847 return false;
848 }
850 // Make the line break disappear and see if that changes the block's
851 // height. Yes, this is an absurd hack. We have to reset height etc. on
852 // the reference node because otherwise its height won't change if it's not
853 // auto.
854 var ref = br.parentNode;
855 while (getComputedStyle(ref).display == "inline") {
856 ref = ref.parentNode;
857 }
858 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
859 ref.style.height = "auto";
860 ref.style.maxHeight = "none";
861 ref.style.minHeight = "0";
862 var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null;
863 var origHeight = ref.offsetHeight;
864 if (origHeight == 0) {
865 throw "isExtraneousLineBreak: original height is zero, bug?";
866 }
867 br.setAttribute("style", "display:none");
868 var finalHeight = ref.offsetHeight;
869 if (refStyle === null) {
870 // Without the setAttribute() line, removeAttribute() doesn't work in
871 // Chrome 14 dev. I have no idea why.
872 ref.setAttribute("style", "");
873 ref.removeAttribute("style");
874 } else {
875 ref.setAttribute("style", refStyle);
876 }
877 if (brStyle === null) {
878 br.removeAttribute("style");
879 } else {
880 br.setAttribute("style", brStyle);
881 }
883 return origHeight == finalHeight;
884 }
886 // "A whitespace node is either a Text node whose data is the empty string; or
887 // a Text node whose data consists only of one or more tabs (0x0009), line
888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
889 // parent is an Element whose resolved value for "white-space" is "normal" or
890 // "nowrap"; or a Text node whose data consists only of one or more tabs
891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
892 // parent is an Element whose resolved value for "white-space" is "pre-line"."
893 function isWhitespaceNode(node) {
894 return node
895 && node.nodeType == Node.TEXT_NODE
896 && (node.data == ""
897 || (
898 /^[\t\n\r ]+$/.test(node.data)
899 && node.parentNode
900 && node.parentNode.nodeType == Node.ELEMENT_NODE
901 && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1
902 ) || (
903 /^[\t\r ]+$/.test(node.data)
904 && node.parentNode
905 && node.parentNode.nodeType == Node.ELEMENT_NODE
906 && getComputedStyle(node.parentNode).whiteSpace == "pre-line"
907 ));
908 }
910 // "node is a collapsed whitespace node if the following algorithm returns
911 // true:"
912 function isCollapsedWhitespaceNode(node) {
913 // "If node is not a whitespace node, return false."
914 if (!isWhitespaceNode(node)) {
915 return false;
916 }
918 // "If node's data is the empty string, return true."
919 if (node.data == "") {
920 return true;
921 }
923 // "Let ancestor be node's parent."
924 var ancestor = node.parentNode;
926 // "If ancestor is null, return true."
927 if (!ancestor) {
928 return true;
929 }
931 // "If the "display" property of some ancestor of node has resolved value
932 // "none", return true."
933 if (getAncestors(node).some(function(ancestor) {
934 return ancestor.nodeType == Node.ELEMENT_NODE
935 && getComputedStyle(ancestor).display == "none";
936 })) {
937 return true;
938 }
940 // "While ancestor is not a block node and its parent is not null, set
941 // ancestor to its parent."
942 while (!isBlockNode(ancestor)
943 && ancestor.parentNode) {
944 ancestor = ancestor.parentNode;
945 }
947 // "Let reference be node."
948 var reference = node;
950 // "While reference is a descendant of ancestor:"
951 while (reference != ancestor) {
952 // "Let reference be the node before it in tree order."
953 reference = previousNode(reference);
955 // "If reference is a block node or a br, return true."
956 if (isBlockNode(reference)
957 || isHtmlElement(reference, "br")) {
958 return true;
959 }
961 // "If reference is a Text node that is not a whitespace node, or is an
962 // img, break from this loop."
963 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
964 || isHtmlElement(reference, "img")) {
965 break;
966 }
967 }
969 // "Let reference be node."
970 reference = node;
972 // "While reference is a descendant of ancestor:"
973 var stop = nextNodeDescendants(ancestor);
974 while (reference != stop) {
975 // "Let reference be the node after it in tree order, or null if there
976 // is no such node."
977 reference = nextNode(reference);
979 // "If reference is a block node or a br, return true."
980 if (isBlockNode(reference)
981 || isHtmlElement(reference, "br")) {
982 return true;
983 }
985 // "If reference is a Text node that is not a whitespace node, or is an
986 // img, break from this loop."
987 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
988 || isHtmlElement(reference, "img")) {
989 break;
990 }
991 }
993 // "Return false."
994 return false;
995 }
997 // "Something is visible if it is a node that either is a block node, or a Text
998 // node that is not a collapsed whitespace node, or an img, or a br that is not
999 // an extraneous line break, or any node with a visible descendant; excluding
1000 // any node with an ancestor container Element whose "display" property has
1001 // resolved value "none"."
1002 function isVisible(node) {
1003 if (!node) {
1004 return false;
1005 }
1007 if (getAncestors(node).concat(node)
1008 .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE })
1009 .some(function(node) { return getComputedStyle(node).display == "none" })) {
1010 return false;
1011 }
1013 if (isBlockNode(node)
1014 || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
1015 || isHtmlElement(node, "img")
1016 || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) {
1017 return true;
1018 }
1020 for (var i = 0; i < node.childNodes.length; i++) {
1021 if (isVisible(node.childNodes[i])) {
1022 return true;
1023 }
1024 }
1026 return false;
1027 }
1029 // "Something is invisible if it is a node that is not visible."
1030 function isInvisible(node) {
1031 return node && !isVisible(node);
1032 }
1034 // "A collapsed block prop is either a collapsed line break that is not an
1035 // extraneous line break, or an Element that is an inline node and whose
1036 // children are all either invisible or collapsed block props and that has at
1037 // least one child that is a collapsed block prop."
1038 function isCollapsedBlockProp(node) {
1039 if (isCollapsedLineBreak(node)
1040 && !isExtraneousLineBreak(node)) {
1041 return true;
1042 }
1044 if (!isInlineNode(node)
1045 || node.nodeType != Node.ELEMENT_NODE) {
1046 return false;
1047 }
1049 var hasCollapsedBlockPropChild = false;
1050 for (var i = 0; i < node.childNodes.length; i++) {
1051 if (!isInvisible(node.childNodes[i])
1052 && !isCollapsedBlockProp(node.childNodes[i])) {
1053 return false;
1054 }
1055 if (isCollapsedBlockProp(node.childNodes[i])) {
1056 hasCollapsedBlockPropChild = true;
1057 }
1058 }
1060 return hasCollapsedBlockPropChild;
1061 }
1063 // "The active range is the range of the selection given by calling
1064 // getSelection() on the context object. (Thus the active range may be null.)"
1065 //
1066 // We cheat and return globalRange if that's defined. We also ensure that the
1067 // active range meets the requirements that selection boundary points are
1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
1069 // descend from a Document.
1070 function getActiveRange() {
1071 var ret;
1072 if (globalRange) {
1073 ret = globalRange;
1074 } else if (getSelection().rangeCount) {
1075 ret = getSelection().getRangeAt(0);
1076 } else {
1077 return null;
1078 }
1079 if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1
1080 || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1
1081 || !ret.startContainer.ownerDocument
1082 || !ret.endContainer.ownerDocument
1083 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
1084 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
1085 throw "Invalid active range; test bug?";
1086 }
1087 return ret;
1088 }
1090 // "For some commands, each HTMLDocument must have a boolean state override
1091 // and/or a string value override. These do not change the command's state or
1092 // value, but change the way some algorithms behave, as specified in those
1093 // algorithms' definitions. Initially, both must be unset for every command.
1094 // Whenever the number of ranges in the Selection changes to something
1095 // different, and whenever a boundary point of the range at a given index in
1096 // the Selection changes to something different, the state override and value
1097 // override must be unset for every command."
1098 //
1099 // We implement this crudely by using setters and getters. To verify that the
1100 // selection hasn't changed, we copy the active range and just check the
1101 // endpoints match. This isn't really correct, but it's good enough for us.
1102 // Unset state/value overrides are undefined. We put everything in a function
1103 // so no one can access anything except via the provided functions, since
1104 // otherwise callers might mistakenly use outdated overrides (if the selection
1105 // has changed).
1106 var getStateOverride, setStateOverride, unsetStateOverride,
1107 getValueOverride, setValueOverride, unsetValueOverride;
1108 (function() {
1109 var stateOverrides = {};
1110 var valueOverrides = {};
1111 var storedRange = null;
1113 function resetOverrides() {
1114 if (!storedRange
1115 || storedRange.startContainer != getActiveRange().startContainer
1116 || storedRange.endContainer != getActiveRange().endContainer
1117 || storedRange.startOffset != getActiveRange().startOffset
1118 || storedRange.endOffset != getActiveRange().endOffset) {
1119 stateOverrides = {};
1120 valueOverrides = {};
1121 storedRange = getActiveRange().cloneRange();
1122 }
1123 }
1125 getStateOverride = function(command) {
1126 resetOverrides();
1127 return stateOverrides[command];
1128 };
1130 setStateOverride = function(command, newState) {
1131 resetOverrides();
1132 stateOverrides[command] = newState;
1133 };
1135 unsetStateOverride = function(command) {
1136 resetOverrides();
1137 delete stateOverrides[command];
1138 }
1140 getValueOverride = function(command) {
1141 resetOverrides();
1142 return valueOverrides[command];
1143 }
1145 // "The value override for the backColor command must be the same as the
1146 // value override for the hiliteColor command, such that setting one sets
1147 // the other to the same thing and unsetting one unsets the other."
1148 setValueOverride = function(command, newValue) {
1149 resetOverrides();
1150 valueOverrides[command] = newValue;
1151 if (command == "backcolor") {
1152 valueOverrides.hilitecolor = newValue;
1153 } else if (command == "hilitecolor") {
1154 valueOverrides.backcolor = newValue;
1155 }
1156 }
1158 unsetValueOverride = function(command) {
1159 resetOverrides();
1160 delete valueOverrides[command];
1161 if (command == "backcolor") {
1162 delete valueOverrides.hilitecolor;
1163 } else if (command == "hilitecolor") {
1164 delete valueOverrides.backcolor;
1165 }
1166 }
1167 })();
1169 //@}
1171 /////////////////////////////
1172 ///// Common algorithms /////
1173 /////////////////////////////
1175 ///// Assorted common algorithms /////
1176 //@{
1178 // Magic array of extra ranges whose endpoints we want to preserve.
1179 var extraRanges = [];
1181 function movePreservingRanges(node, newParent, newIndex) {
1182 // For convenience, I allow newIndex to be -1 to mean "insert at the end".
1183 if (newIndex == -1) {
1184 newIndex = newParent.childNodes.length;
1185 }
1187 // "When the user agent is to move a Node to a new location, preserving
1188 // ranges, it must remove the Node from its original parent (if any), then
1189 // insert it in the new location. In doing so, however, it must ignore the
1190 // regular range mutation rules, and instead follow these rules:"
1192 // "Let node be the moved Node, old parent and old index be the old parent
1193 // (which may be null) and index, and new parent and new index be the new
1194 // parent and index."
1195 var oldParent = node.parentNode;
1196 var oldIndex = getNodeIndex(node);
1198 // We preserve the global range object, the ranges in the selection, and
1199 // any range that's in the extraRanges array. Any other ranges won't get
1200 // updated, because we have no references to them.
1201 var ranges = [globalRange].concat(extraRanges);
1202 for (var i = 0; i < getSelection().rangeCount; i++) {
1203 ranges.push(getSelection().getRangeAt(i));
1204 }
1205 var boundaryPoints = [];
1206 ranges.forEach(function(range) {
1207 boundaryPoints.push([range.startContainer, range.startOffset]);
1208 boundaryPoints.push([range.endContainer, range.endOffset]);
1209 });
1211 boundaryPoints.forEach(function(boundaryPoint) {
1212 // "If a boundary point's node is the same as or a descendant of node,
1213 // leave it unchanged, so it moves to the new location."
1214 //
1215 // No modifications necessary.
1217 // "If a boundary point's node is new parent and its offset is greater
1218 // than new index, add one to its offset."
1219 if (boundaryPoint[0] == newParent
1220 && boundaryPoint[1] > newIndex) {
1221 boundaryPoint[1]++;
1222 }
1224 // "If a boundary point's node is old parent and its offset is old index or
1225 // old index + 1, set its node to new parent and add new index − old index
1226 // to its offset."
1227 if (boundaryPoint[0] == oldParent
1228 && (boundaryPoint[1] == oldIndex
1229 || boundaryPoint[1] == oldIndex + 1)) {
1230 boundaryPoint[0] = newParent;
1231 boundaryPoint[1] += newIndex - oldIndex;
1232 }
1234 // "If a boundary point's node is old parent and its offset is greater than
1235 // old index + 1, subtract one from its offset."
1236 if (boundaryPoint[0] == oldParent
1237 && boundaryPoint[1] > oldIndex + 1) {
1238 boundaryPoint[1]--;
1239 }
1240 });
1242 // Now actually move it and preserve the ranges.
1243 if (newParent.childNodes.length == newIndex) {
1244 newParent.appendChild(node);
1245 } else {
1246 newParent.insertBefore(node, newParent.childNodes[newIndex]);
1247 }
1249 globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
1250 globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
1252 for (var i = 0; i < extraRanges.length; i++) {
1253 extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]);
1254 extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]);
1255 }
1257 getSelection().removeAllRanges();
1258 for (var i = 1 + extraRanges.length; i < ranges.length; i++) {
1259 var newRange = document.createRange();
1260 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
1261 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
1262 getSelection().addRange(newRange);
1263 }
1264 }
1266 function setTagName(element, newName) {
1267 // "If element is an HTML element with local name equal to new name, return
1268 // element."
1269 if (isHtmlElement(element, newName.toUpperCase())) {
1270 return element;
1271 }
1273 // "If element's parent is null, return element."
1274 if (!element.parentNode) {
1275 return element;
1276 }
1278 // "Let replacement element be the result of calling createElement(new
1279 // name) on the ownerDocument of element."
1280 var replacementElement = element.ownerDocument.createElement(newName);
1282 // "Insert replacement element into element's parent immediately before
1283 // element."
1284 element.parentNode.insertBefore(replacementElement, element);
1286 // "Copy all attributes of element to replacement element, in order."
1287 for (var i = 0; i < element.attributes.length; i++) {
1288 replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value);
1289 }
1291 // "While element has children, append the first child of element as the
1292 // last child of replacement element, preserving ranges."
1293 while (element.childNodes.length) {
1294 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length);
1295 }
1297 // "Remove element from its parent."
1298 element.parentNode.removeChild(element);
1300 // "Return replacement element."
1301 return replacementElement;
1302 }
1304 function removeExtraneousLineBreaksBefore(node) {
1305 // "Let ref be the previousSibling of node."
1306 var ref = node.previousSibling;
1308 // "If ref is null, abort these steps."
1309 if (!ref) {
1310 return;
1311 }
1313 // "While ref has children, set ref to its lastChild."
1314 while (ref.hasChildNodes()) {
1315 ref = ref.lastChild;
1316 }
1318 // "While ref is invisible but not an extraneous line break, and ref does
1319 // not equal node's parent, set ref to the node before it in tree order."
1320 while (isInvisible(ref)
1321 && !isExtraneousLineBreak(ref)
1322 && ref != node.parentNode) {
1323 ref = previousNode(ref);
1324 }
1326 // "If ref is an editable extraneous line break, remove it from its
1327 // parent."
1328 if (isEditable(ref)
1329 && isExtraneousLineBreak(ref)) {
1330 ref.parentNode.removeChild(ref);
1331 }
1332 }
1334 function removeExtraneousLineBreaksAtTheEndOf(node) {
1335 // "Let ref be node."
1336 var ref = node;
1338 // "While ref has children, set ref to its lastChild."
1339 while (ref.hasChildNodes()) {
1340 ref = ref.lastChild;
1341 }
1343 // "While ref is invisible but not an extraneous line break, and ref does
1344 // not equal node, set ref to the node before it in tree order."
1345 while (isInvisible(ref)
1346 && !isExtraneousLineBreak(ref)
1347 && ref != node) {
1348 ref = previousNode(ref);
1349 }
1351 // "If ref is an editable extraneous line break:"
1352 if (isEditable(ref)
1353 && isExtraneousLineBreak(ref)) {
1354 // "While ref's parent is editable and invisible, set ref to its
1355 // parent."
1356 while (isEditable(ref.parentNode)
1357 && isInvisible(ref.parentNode)) {
1358 ref = ref.parentNode;
1359 }
1361 // "Remove ref from its parent."
1362 ref.parentNode.removeChild(ref);
1363 }
1364 }
1366 // "To remove extraneous line breaks from a node, first remove extraneous line
1367 // breaks before it, then remove extraneous line breaks at the end of it."
1368 function removeExtraneousLineBreaksFrom(node) {
1369 removeExtraneousLineBreaksBefore(node);
1370 removeExtraneousLineBreaksAtTheEndOf(node);
1371 }
1373 //@}
1374 ///// Wrapping a list of nodes /////
1375 //@{
1377 function wrap(nodeList, siblingCriteria, newParentInstructions) {
1378 // "If not provided, sibling criteria returns false and new parent
1379 // instructions returns null."
1380 if (typeof siblingCriteria == "undefined") {
1381 siblingCriteria = function() { return false };
1382 }
1383 if (typeof newParentInstructions == "undefined") {
1384 newParentInstructions = function() { return null };
1385 }
1387 // "If every member of node list is invisible, and none is a br, return
1388 // null and abort these steps."
1389 if (nodeList.every(isInvisible)
1390 && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) {
1391 return null;
1392 }
1394 // "If node list's first member's parent is null, return null and abort
1395 // these steps."
1396 if (!nodeList[0].parentNode) {
1397 return null;
1398 }
1400 // "If node list's last member is an inline node that's not a br, and node
1401 // list's last member's nextSibling is a br, append that br to node list."
1402 if (isInlineNode(nodeList[nodeList.length - 1])
1403 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
1404 && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
1405 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
1406 }
1408 // "While node list's first member's previousSibling is invisible, prepend
1409 // it to node list."
1410 while (isInvisible(nodeList[0].previousSibling)) {
1411 nodeList.unshift(nodeList[0].previousSibling);
1412 }
1414 // "While node list's last member's nextSibling is invisible, append it to
1415 // node list."
1416 while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) {
1417 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
1418 }
1420 // "If the previousSibling of the first member of node list is editable and
1421 // running sibling criteria on it returns true, let new parent be the
1422 // previousSibling of the first member of node list."
1423 var newParent;
1424 if (isEditable(nodeList[0].previousSibling)
1425 && siblingCriteria(nodeList[0].previousSibling)) {
1426 newParent = nodeList[0].previousSibling;
1428 // "Otherwise, if the nextSibling of the last member of node list is
1429 // editable and running sibling criteria on it returns true, let new parent
1430 // be the nextSibling of the last member of node list."
1431 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
1432 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
1433 newParent = nodeList[nodeList.length - 1].nextSibling;
1435 // "Otherwise, run new parent instructions, and let new parent be the
1436 // result."
1437 } else {
1438 newParent = newParentInstructions();
1439 }
1441 // "If new parent is null, abort these steps and return null."
1442 if (!newParent) {
1443 return null;
1444 }
1446 // "If new parent's parent is null:"
1447 if (!newParent.parentNode) {
1448 // "Insert new parent into the parent of the first member of node list
1449 // immediately before the first member of node list."
1450 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
1452 // "If any range has a boundary point with node equal to the parent of
1453 // new parent and offset equal to the index of new parent, add one to
1454 // that boundary point's offset."
1455 //
1456 // Only try to fix the global range.
1457 if (globalRange.startContainer == newParent.parentNode
1458 && globalRange.startOffset == getNodeIndex(newParent)) {
1459 globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1);
1460 }
1461 if (globalRange.endContainer == newParent.parentNode
1462 && globalRange.endOffset == getNodeIndex(newParent)) {
1463 globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1);
1464 }
1465 }
1467 // "Let original parent be the parent of the first member of node list."
1468 var originalParent = nodeList[0].parentNode;
1470 // "If new parent is before the first member of node list in tree order:"
1471 if (isBefore(newParent, nodeList[0])) {
1472 // "If new parent is not an inline node, but the last visible child of
1473 // new parent and the first visible member of node list are both inline
1474 // nodes, and the last child of new parent is not a br, call
1475 // createElement("br") on the ownerDocument of new parent and append
1476 // the result as the last child of new parent."
1477 if (!isInlineNode(newParent)
1478 && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0])
1479 && isInlineNode(nodeList.filter(isVisible)[0])
1480 && !isHtmlElement(newParent.lastChild, "BR")) {
1481 newParent.appendChild(newParent.ownerDocument.createElement("br"));
1482 }
1484 // "For each node in node list, append node as the last child of new
1485 // parent, preserving ranges."
1486 for (var i = 0; i < nodeList.length; i++) {
1487 movePreservingRanges(nodeList[i], newParent, -1);
1488 }
1490 // "Otherwise:"
1491 } else {
1492 // "If new parent is not an inline node, but the first visible child of
1493 // new parent and the last visible member of node list are both inline
1494 // nodes, and the last member of node list is not a br, call
1495 // createElement("br") on the ownerDocument of new parent and insert
1496 // the result as the first child of new parent."
1497 if (!isInlineNode(newParent)
1498 && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0])
1499 && isInlineNode(nodeList.filter(isVisible).slice(-1)[0])
1500 && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) {
1501 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
1502 }
1504 // "For each node in node list, in reverse order, insert node as the
1505 // first child of new parent, preserving ranges."
1506 for (var i = nodeList.length - 1; i >= 0; i--) {
1507 movePreservingRanges(nodeList[i], newParent, 0);
1508 }
1509 }
1511 // "If original parent is editable and has no children, remove it from its
1512 // parent."
1513 if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
1514 originalParent.parentNode.removeChild(originalParent);
1515 }
1517 // "If new parent's nextSibling is editable and running sibling criteria on
1518 // it returns true:"
1519 if (isEditable(newParent.nextSibling)
1520 && siblingCriteria(newParent.nextSibling)) {
1521 // "If new parent is not an inline node, but new parent's last child
1522 // and new parent's nextSibling's first child are both inline nodes,
1523 // and new parent's last child is not a br, call createElement("br") on
1524 // the ownerDocument of new parent and append the result as the last
1525 // child of new parent."
1526 if (!isInlineNode(newParent)
1527 && isInlineNode(newParent.lastChild)
1528 && isInlineNode(newParent.nextSibling.firstChild)
1529 && !isHtmlElement(newParent.lastChild, "BR")) {
1530 newParent.appendChild(newParent.ownerDocument.createElement("br"));
1531 }
1533 // "While new parent's nextSibling has children, append its first child
1534 // as the last child of new parent, preserving ranges."
1535 while (newParent.nextSibling.hasChildNodes()) {
1536 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1);
1537 }
1539 // "Remove new parent's nextSibling from its parent."
1540 newParent.parentNode.removeChild(newParent.nextSibling);
1541 }
1543 // "Remove extraneous line breaks from new parent."
1544 removeExtraneousLineBreaksFrom(newParent);
1546 // "Return new parent."
1547 return newParent;
1548 }
1551 //@}
1552 ///// Allowed children /////
1553 //@{
1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo",
1561 "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
1562 "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
1563 "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
1564 "xmp", "big", "blink", "font", "marquee", "nobr", "tt"];
1566 // "An element with inline contents is an HTML element whose local name is a
1567 // name of an element with inline contents."
1568 function isElementWithInlineContents(node) {
1569 return isHtmlElement(node, namesOfElementsWithInlineContents);
1570 }
1572 function isAllowedChild(child, parent_) {
1573 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
1574 // an HTML element with local name equal to one of those, and child is a
1575 // Text node whose data does not consist solely of space characters, return
1576 // false."
1577 if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1
1578 || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
1579 && typeof child == "object"
1580 && child.nodeType == Node.TEXT_NODE
1581 && !/^[ \t\n\f\r]*$/.test(child.data)) {
1582 return false;
1583 }
1585 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML
1586 // element with local name equal to one of those, and child is not a Text
1587 // node, return false."
1588 if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1
1589 || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"]))
1590 && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) {
1591 return false;
1592 }
1594 // "If child is a Document, DocumentFragment, or DocumentType, return
1595 // false."
1596 if (typeof child == "object"
1597 && (child.nodeType == Node.DOCUMENT_NODE
1598 || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE
1599 || child.nodeType == Node.DOCUMENT_TYPE_NODE)) {
1600 return false;
1601 }
1603 // "If child is an HTML element, set child to the local name of child."
1604 if (isHtmlElement(child)) {
1605 child = child.tagName.toLowerCase();
1606 }
1608 // "If child is not a string, return true."
1609 if (typeof child != "string") {
1610 return true;
1611 }
1613 // "If parent is an HTML element:"
1614 if (isHtmlElement(parent_)) {
1615 // "If child is "a", and parent or some ancestor of parent is an a,
1616 // return false."
1617 //
1618 // "If child is a prohibited paragraph child name and parent or some
1619 // ancestor of parent is an element with inline contents, return
1620 // false."
1621 //
1622 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
1623 // some ancestor of parent is an HTML element with local name "h1",
1624 // "h2", "h3", "h4", "h5", or "h6", return false."
1625 var ancestor = parent_;
1626 while (ancestor) {
1627 if (child == "a" && isHtmlElement(ancestor, "a")) {
1628 return false;
1629 }
1630 if (prohibitedParagraphChildNames.indexOf(child) != -1
1631 && isElementWithInlineContents(ancestor)) {
1632 return false;
1633 }
1634 if (/^h[1-6]$/.test(child)
1635 && isHtmlElement(ancestor)
1636 && /^H[1-6]$/.test(ancestor.tagName)) {
1637 return false;
1638 }
1639 ancestor = ancestor.parentNode;
1640 }
1642 // "Let parent be the local name of parent."
1643 parent_ = parent_.tagName.toLowerCase();
1644 }
1646 // "If parent is an Element or DocumentFragment, return true."
1647 if (typeof parent_ == "object"
1648 && (parent_.nodeType == Node.ELEMENT_NODE
1649 || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
1650 return true;
1651 }
1653 // "If parent is not a string, return false."
1654 if (typeof parent_ != "string") {
1655 return false;
1656 }
1658 // "If parent is on the left-hand side of an entry on the following list,
1659 // then return true if child is listed on the right-hand side of that
1660 // entry, and false otherwise."
1661 switch (parent_) {
1662 case "colgroup":
1663 return child == "col";
1664 case "table":
1665 return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1;
1666 case "tbody":
1667 case "thead":
1668 case "tfoot":
1669 return ["td", "th", "tr"].indexOf(child) != -1;
1670 case "tr":
1671 return ["td", "th"].indexOf(child) != -1;
1672 case "dl":
1673 return ["dt", "dd"].indexOf(child) != -1;
1674 case "dir":
1675 case "ol":
1676 case "ul":
1677 return ["dir", "li", "ol", "ul"].indexOf(child) != -1;
1678 case "hgroup":
1679 return /^h[1-6]$/.test(child);
1680 }
1682 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
1683 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
1684 // false."
1685 if (["body", "caption", "col", "colgroup", "frame", "frameset", "head",
1686 "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) {
1687 return false;
1688 }
1690 // "If child is "dd" or "dt" and parent is not "dl", return false."
1691 if (["dd", "dt"].indexOf(child) != -1
1692 && parent_ != "dl") {
1693 return false;
1694 }
1696 // "If child is "li" and parent is not "ol" or "ul", return false."
1697 if (child == "li"
1698 && parent_ != "ol"
1699 && parent_ != "ul") {
1700 return false;
1701 }
1703 // "If parent is on the left-hand side of an entry on the following list
1704 // and child is listed on the right-hand side of that entry, return false."
1705 var table = [
1706 [["a"], ["a"]],
1707 [["dd", "dt"], ["dd", "dt"]],
1708 [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]],
1709 [["li"], ["li"]],
1710 [["nobr"], ["nobr"]],
1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]],
1713 ];
1714 for (var i = 0; i < table.length; i++) {
1715 if (table[i][0].indexOf(parent_) != -1
1716 && table[i][1].indexOf(child) != -1) {
1717 return false;
1718 }
1719 }
1721 // "Return true."
1722 return true;
1723 }
1726 //@}
1728 //////////////////////////////////////
1729 ///// Inline formatting commands /////
1730 //////////////////////////////////////
1732 ///// Inline formatting command definitions /////
1733 //@{
1735 // "A node node is effectively contained in a range range if range is not
1736 // collapsed, and at least one of the following holds:"
1737 function isEffectivelyContained(node, range) {
1738 if (range.collapsed) {
1739 return false;
1740 }
1742 // "node is contained in range."
1743 if (isContained(node, range)) {
1744 return true;
1745 }
1747 // "node is range's start node, it is a Text node, and its length is
1748 // different from range's start offset."
1749 if (node == range.startContainer
1750 && node.nodeType == Node.TEXT_NODE
1751 && getNodeLength(node) != range.startOffset) {
1752 return true;
1753 }
1755 // "node is range's end node, it is a Text node, and range's end offset is
1756 // not 0."
1757 if (node == range.endContainer
1758 && node.nodeType == Node.TEXT_NODE
1759 && range.endOffset != 0) {
1760 return true;
1761 }
1763 // "node has at least one child; and all its children are effectively
1764 // contained in range; and either range's start node is not a descendant of
1765 // node or is not a Text node or range's start offset is zero; and either
1766 // range's end node is not a descendant of node or is not a Text node or
1767 // range's end offset is its end node's length."
1768 if (node.hasChildNodes()
1769 && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) })
1770 && (!isDescendant(range.startContainer, node)
1771 || range.startContainer.nodeType != Node.TEXT_NODE
1772 || range.startOffset == 0)
1773 && (!isDescendant(range.endContainer, node)
1774 || range.endContainer.nodeType != Node.TEXT_NODE
1775 || range.endOffset == getNodeLength(range.endContainer))) {
1776 return true;
1777 }
1779 return false;
1780 }
1782 // Like get(All)ContainedNodes(), but for effectively contained nodes.
1783 function getEffectivelyContainedNodes(range, condition) {
1784 if (typeof condition == "undefined") {
1785 condition = function() { return true };
1786 }
1787 var node = range.startContainer;
1788 while (isEffectivelyContained(node.parentNode, range)) {
1789 node = node.parentNode;
1790 }
1792 var stop = nextNodeDescendants(range.endContainer);
1794 var nodeList = [];
1795 while (isBefore(node, stop)) {
1796 if (isEffectivelyContained(node, range)
1797 && condition(node)) {
1798 nodeList.push(node);
1799 node = nextNodeDescendants(node);
1800 continue;
1801 }
1802 node = nextNode(node);
1803 }
1804 return nodeList;
1805 }
1807 function getAllEffectivelyContainedNodes(range, condition) {
1808 if (typeof condition == "undefined") {
1809 condition = function() { return true };
1810 }
1811 var node = range.startContainer;
1812 while (isEffectivelyContained(node.parentNode, range)) {
1813 node = node.parentNode;
1814 }
1816 var stop = nextNodeDescendants(range.endContainer);
1818 var nodeList = [];
1819 while (isBefore(node, stop)) {
1820 if (isEffectivelyContained(node, range)
1821 && condition(node)) {
1822 nodeList.push(node);
1823 }
1824 node = nextNode(node);
1825 }
1826 return nodeList;
1827 }
1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
1830 // with no attributes except possibly style; or a font element with no
1831 // attributes except possibly style, color, face, and/or size; or an a element
1832 // with no attributes except possibly style and/or href."
1833 function isModifiableElement(node) {
1834 if (!isHtmlElement(node)) {
1835 return false;
1836 }
1838 if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) {
1839 if (node.attributes.length == 0) {
1840 return true;
1841 }
1843 if (node.attributes.length == 1
1844 && node.hasAttribute("style")) {
1845 return true;
1846 }
1847 }
1849 if (node.tagName == "FONT" || node.tagName == "A") {
1850 var numAttrs = node.attributes.length;
1852 if (node.hasAttribute("style")) {
1853 numAttrs--;
1854 }
1856 if (node.tagName == "FONT") {
1857 if (node.hasAttribute("color")) {
1858 numAttrs--;
1859 }
1861 if (node.hasAttribute("face")) {
1862 numAttrs--;
1863 }
1865 if (node.hasAttribute("size")) {
1866 numAttrs--;
1867 }
1868 }
1870 if (node.tagName == "A"
1871 && node.hasAttribute("href")) {
1872 numAttrs--;
1873 }
1875 if (numAttrs == 0) {
1876 return true;
1877 }
1878 }
1880 return false;
1881 }
1883 function isSimpleModifiableElement(node) {
1884 // "A simple modifiable element is an HTML element for which at least one
1885 // of the following holds:"
1886 if (!isHtmlElement(node)) {
1887 return false;
1888 }
1890 // Only these elements can possibly be a simple modifiable element.
1891 if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) {
1892 return false;
1893 }
1895 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
1896 // element with no attributes."
1897 if (node.attributes.length == 0) {
1898 return true;
1899 }
1901 // If it's got more than one attribute, everything after this fails.
1902 if (node.attributes.length > 1) {
1903 return false;
1904 }
1906 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
1907 // element with exactly one attribute, which is style, which sets no CSS
1908 // properties (including invalid or unrecognized properties)."
1909 //
1910 // Not gonna try for invalid or unrecognized.
1911 if (node.hasAttribute("style")
1912 && node.style.length == 0) {
1913 return true;
1914 }
1916 // "It is an a element with exactly one attribute, which is href."
1917 if (node.tagName == "A"
1918 && node.hasAttribute("href")) {
1919 return true;
1920 }
1922 // "It is a font element with exactly one attribute, which is either color,
1923 // face, or size."
1924 if (node.tagName == "FONT"
1925 && (node.hasAttribute("color")
1926 || node.hasAttribute("face")
1927 || node.hasAttribute("size")
1928 )) {
1929 return true;
1930 }
1932 // "It is a b or strong element with exactly one attribute, which is style,
1933 // and the style attribute sets exactly one CSS property (including invalid
1934 // or unrecognized properties), which is "font-weight"."
1935 if ((node.tagName == "B" || node.tagName == "STRONG")
1936 && node.hasAttribute("style")
1937 && node.style.length == 1
1938 && node.style.fontWeight != "") {
1939 return true;
1940 }
1942 // "It is an i or em element with exactly one attribute, which is style,
1943 // and the style attribute sets exactly one CSS property (including invalid
1944 // or unrecognized properties), which is "font-style"."
1945 if ((node.tagName == "I" || node.tagName == "EM")
1946 && node.hasAttribute("style")
1947 && node.style.length == 1
1948 && node.style.fontStyle != "") {
1949 return true;
1950 }
1952 // "It is an a, font, or span element with exactly one attribute, which is
1953 // style, and the style attribute sets exactly one CSS property (including
1954 // invalid or unrecognized properties), and that property is not
1955 // "text-decoration"."
1956 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
1957 && node.hasAttribute("style")
1958 && node.style.length == 1
1959 && node.style.textDecoration == "") {
1960 return true;
1961 }
1963 // "It is an a, font, s, span, strike, or u element with exactly one
1964 // attribute, which is style, and the style attribute sets exactly one CSS
1965 // property (including invalid or unrecognized properties), which is
1966 // "text-decoration", which is set to "line-through" or "underline" or
1967 // "overline" or "none"."
1968 //
1969 // The weird extra node.style.length check is for Firefox, which as of
1970 // 8.0a2 has annoying and weird behavior here.
1971 if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1
1972 && node.hasAttribute("style")
1973 && (node.style.length == 1
1974 || (node.style.length == 4
1975 && "MozTextBlink" in node.style
1976 && "MozTextDecorationColor" in node.style
1977 && "MozTextDecorationLine" in node.style
1978 && "MozTextDecorationStyle" in node.style)
1979 )
1980 && (node.style.textDecoration == "line-through"
1981 || node.style.textDecoration == "underline"
1982 || node.style.textDecoration == "overline"
1983 || node.style.textDecoration == "none")) {
1984 return true;
1985 }
1987 return false;
1988 }
1990 // "A formattable node is an editable visible node that is either a Text node,
1991 // an img, or a br."
1992 function isFormattableNode(node) {
1993 return isEditable(node)
1994 && isVisible(node)
1995 && (node.nodeType == Node.TEXT_NODE
1996 || isHtmlElement(node, ["img", "br"]));
1997 }
1999 // "Two quantities are equivalent values for a command if either both are null,
2000 // or both are strings and they're equal and the command does not define any
2001 // equivalent values, or both are strings and the command defines equivalent
2002 // values and they match the definition."
2003 function areEquivalentValues(command, val1, val2) {
2004 if (val1 === null && val2 === null) {
2005 return true;
2006 }
2008 if (typeof val1 == "string"
2009 && typeof val2 == "string"
2010 && val1 == val2
2011 && !("equivalentValues" in commands[command])) {
2012 return true;
2013 }
2015 if (typeof val1 == "string"
2016 && typeof val2 == "string"
2017 && "equivalentValues" in commands[command]
2018 && commands[command].equivalentValues(val1, val2)) {
2019 return true;
2020 }
2022 return false;
2023 }
2025 // "Two quantities are loosely equivalent values for a command if either they
2026 // are equivalent values for the command, or if the command is the fontSize
2027 // command; one of the quantities is one of "x-small", "small", "medium",
2028 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
2029 // the resolved value of "font-size" on a font element whose size attribute has
2030 // the corresponding value set ("1" through "7" respectively)."
2031 function areLooselyEquivalentValues(command, val1, val2) {
2032 if (areEquivalentValues(command, val1, val2)) {
2033 return true;
2034 }
2036 if (command != "fontsize"
2037 || typeof val1 != "string"
2038 || typeof val2 != "string") {
2039 return false;
2040 }
2042 // Static variables in JavaScript?
2043 var callee = areLooselyEquivalentValues;
2044 if (callee.sizeMap === undefined) {
2045 callee.sizeMap = {};
2046 var font = document.createElement("font");
2047 document.body.appendChild(font);
2048 ["x-small", "small", "medium", "large", "x-large", "xx-large",
2049 "xxx-large"].forEach(function(keyword) {
2050 font.size = cssSizeToLegacy(keyword);
2051 callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
2052 });
2053 document.body.removeChild(font);
2054 }
2056 return val1 === callee.sizeMap[val2]
2057 || val2 === callee.sizeMap[val1];
2058 }
2060 //@}
2061 ///// Assorted inline formatting command algorithms /////
2062 //@{
2064 function getEffectiveCommandValue(node, command) {
2065 // "If neither node nor its parent is an Element, return null."
2066 if (node.nodeType != Node.ELEMENT_NODE
2067 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
2068 return null;
2069 }
2071 // "If node is not an Element, return the effective command value of its
2072 // parent for command."
2073 if (node.nodeType != Node.ELEMENT_NODE) {
2074 return getEffectiveCommandValue(node.parentNode, command);
2075 }
2077 // "If command is "createLink" or "unlink":"
2078 if (command == "createlink" || command == "unlink") {
2079 // "While node is not null, and is not an a element that has an href
2080 // attribute, set node to its parent."
2081 while (node
2082 && (!isHtmlElement(node)
2083 || node.tagName != "A"
2084 || !node.hasAttribute("href"))) {
2085 node = node.parentNode;
2086 }
2088 // "If node is null, return null."
2089 if (!node) {
2090 return null;
2091 }
2093 // "Return the value of node's href attribute."
2094 return node.getAttribute("href");
2095 }
2097 // "If command is "backColor" or "hiliteColor":"
2098 if (command == "backcolor"
2099 || command == "hilitecolor") {
2100 // "While the resolved value of "background-color" on node is any
2101 // fully transparent value, and node's parent is an Element, set
2102 // node to its parent."
2103 //
2104 // Another lame hack to avoid flawed APIs.
2105 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
2106 || getComputedStyle(node).backgroundColor === ""
2107 || getComputedStyle(node).backgroundColor == "transparent")
2108 && node.parentNode
2109 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
2110 node = node.parentNode;
2111 }
2113 // "Return the resolved value of "background-color" for node."
2114 return getComputedStyle(node).backgroundColor;
2115 }
2117 // "If command is "subscript" or "superscript":"
2118 if (command == "subscript" || command == "superscript") {
2119 // "Let affected by subscript and affected by superscript be two
2120 // boolean variables, both initially false."
2121 var affectedBySubscript = false;
2122 var affectedBySuperscript = false;
2124 // "While node is an inline node:"
2125 while (isInlineNode(node)) {
2126 var verticalAlign = getComputedStyle(node).verticalAlign;
2128 // "If node is a sub, set affected by subscript to true."
2129 if (isHtmlElement(node, "sub")) {
2130 affectedBySubscript = true;
2131 // "Otherwise, if node is a sup, set affected by superscript to
2132 // true."
2133 } else if (isHtmlElement(node, "sup")) {
2134 affectedBySuperscript = true;
2135 }
2137 // "Set node to its parent."
2138 node = node.parentNode;
2139 }
2141 // "If affected by subscript and affected by superscript are both true,
2142 // return the string "mixed"."
2143 if (affectedBySubscript && affectedBySuperscript) {
2144 return "mixed";
2145 }
2147 // "If affected by subscript is true, return "subscript"."
2148 if (affectedBySubscript) {
2149 return "subscript";
2150 }
2152 // "If affected by superscript is true, return "superscript"."
2153 if (affectedBySuperscript) {
2154 return "superscript";
2155 }
2157 // "Return null."
2158 return null;
2159 }
2161 // "If command is "strikethrough", and the "text-decoration" property of
2162 // node or any of its ancestors has resolved value containing
2163 // "line-through", return "line-through". Otherwise, return null."
2164 if (command == "strikethrough") {
2165 do {
2166 if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
2167 return "line-through";
2168 }
2169 node = node.parentNode;
2170 } while (node && node.nodeType == Node.ELEMENT_NODE);
2171 return null;
2172 }
2174 // "If command is "underline", and the "text-decoration" property of node
2175 // or any of its ancestors has resolved value containing "underline",
2176 // return "underline". Otherwise, return null."
2177 if (command == "underline") {
2178 do {
2179 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
2180 return "underline";
2181 }
2182 node = node.parentNode;
2183 } while (node && node.nodeType == Node.ELEMENT_NODE);
2184 return null;
2185 }
2187 if (!("relevantCssProperty" in commands[command])) {
2188 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
2189 }
2191 // "Return the resolved value for node of the relevant CSS property for
2192 // command."
2193 return getComputedStyle(node)[commands[command].relevantCssProperty];
2194 }
2196 function getSpecifiedCommandValue(element, command) {
2197 // "If command is "backColor" or "hiliteColor" and element's display
2198 // property does not have resolved value "inline", return null."
2199 if ((command == "backcolor" || command == "hilitecolor")
2200 && getComputedStyle(element).display != "inline") {
2201 return null;
2202 }
2204 // "If command is "createLink" or "unlink":"
2205 if (command == "createlink" || command == "unlink") {
2206 // "If element is an a element and has an href attribute, return the
2207 // value of that attribute."
2208 if (isHtmlElement(element)
2209 && element.tagName == "A"
2210 && element.hasAttribute("href")) {
2211 return element.getAttribute("href");
2212 }
2214 // "Return null."
2215 return null;
2216 }
2218 // "If command is "subscript" or "superscript":"
2219 if (command == "subscript" || command == "superscript") {
2220 // "If element is a sup, return "superscript"."
2221 if (isHtmlElement(element, "sup")) {
2222 return "superscript";
2223 }
2225 // "If element is a sub, return "subscript"."
2226 if (isHtmlElement(element, "sub")) {
2227 return "subscript";
2228 }
2230 // "Return null."
2231 return null;
2232 }
2234 // "If command is "strikethrough", and element has a style attribute set,
2235 // and that attribute sets "text-decoration":"
2236 if (command == "strikethrough"
2237 && element.style.textDecoration != "") {
2238 // "If element's style attribute sets "text-decoration" to a value
2239 // containing "line-through", return "line-through"."
2240 if (element.style.textDecoration.indexOf("line-through") != -1) {
2241 return "line-through";
2242 }
2244 // "Return null."
2245 return null;
2246 }
2248 // "If command is "strikethrough" and element is a s or strike element,
2249 // return "line-through"."
2250 if (command == "strikethrough"
2251 && isHtmlElement(element, ["S", "STRIKE"])) {
2252 return "line-through";
2253 }
2255 // "If command is "underline", and element has a style attribute set, and
2256 // that attribute sets "text-decoration":"
2257 if (command == "underline"
2258 && element.style.textDecoration != "") {
2259 // "If element's style attribute sets "text-decoration" to a value
2260 // containing "underline", return "underline"."
2261 if (element.style.textDecoration.indexOf("underline") != -1) {
2262 return "underline";
2263 }
2265 // "Return null."
2266 return null;
2267 }
2269 // "If command is "underline" and element is a u element, return
2270 // "underline"."
2271 if (command == "underline"
2272 && isHtmlElement(element, "U")) {
2273 return "underline";
2274 }
2276 // "Let property be the relevant CSS property for command."
2277 var property = commands[command].relevantCssProperty;
2279 // "If property is null, return null."
2280 if (property === null) {
2281 return null;
2282 }
2284 // "If element has a style attribute set, and that attribute has the
2285 // effect of setting property, return the value that it sets property to."
2286 if (element.style[property] != "") {
2287 return element.style[property];
2288 }
2290 // "If element is a font element that has an attribute whose effect is
2291 // to create a presentational hint for property, return the value that the
2292 // hint sets property to. (For a size of 7, this will be the non-CSS value
2293 // "xxx-large".)"
2294 if (isHtmlNamespace(element.namespaceURI)
2295 && element.tagName == "FONT") {
2296 if (property == "color" && element.hasAttribute("color")) {
2297 return element.color;
2298 }
2299 if (property == "fontFamily" && element.hasAttribute("face")) {
2300 return element.face;
2301 }
2302 if (property == "fontSize" && element.hasAttribute("size")) {
2303 // This is not even close to correct in general.
2304 var size = parseInt(element.size);
2305 if (size < 1) {
2306 size = 1;
2307 }
2308 if (size > 7) {
2309 size = 7;
2310 }
2311 return {
2312 1: "x-small",
2313 2: "small",
2314 3: "medium",
2315 4: "large",
2316 5: "x-large",
2317 6: "xx-large",
2318 7: "xxx-large"
2319 }[size];
2320 }
2321 }
2323 // "If element is in the following list, and property is equal to the
2324 // CSS property name listed for it, return the string listed for it."
2325 //
2326 // A list follows, whose meaning is copied here.
2327 if (property == "fontWeight"
2328 && (element.tagName == "B" || element.tagName == "STRONG")) {
2329 return "bold";
2330 }
2331 if (property == "fontStyle"
2332 && (element.tagName == "I" || element.tagName == "EM")) {
2333 return "italic";
2334 }
2336 // "Return null."
2337 return null;
2338 }
2340 function reorderModifiableDescendants(node, command, newValue) {
2341 // "Let candidate equal node."
2342 var candidate = node;
2344 // "While candidate is a modifiable element, and candidate has exactly one
2345 // child, and that child is also a modifiable element, and candidate is not
2346 // a simple modifiable element or candidate's specified command value for
2347 // command is not equivalent to new value, set candidate to its child."
2348 while (isModifiableElement(candidate)
2349 && candidate.childNodes.length == 1
2350 && isModifiableElement(candidate.firstChild)
2351 && (!isSimpleModifiableElement(candidate)
2352 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
2353 candidate = candidate.firstChild;
2354 }
2356 // "If candidate is node, or is not a simple modifiable element, or its
2357 // specified command value is not equivalent to new value, or its effective
2358 // command value is not loosely equivalent to new value, abort these
2359 // steps."
2360 if (candidate == node
2361 || !isSimpleModifiableElement(candidate)
2362 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
2363 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
2364 return;
2365 }
2367 // "While candidate has children, insert the first child of candidate into
2368 // candidate's parent immediately before candidate, preserving ranges."
2369 while (candidate.hasChildNodes()) {
2370 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate));
2371 }
2373 // "Insert candidate into node's parent immediately after node."
2374 node.parentNode.insertBefore(candidate, node.nextSibling);
2376 // "Append the node as the last child of candidate, preserving ranges."
2377 movePreservingRanges(node, candidate, -1);
2378 }
2380 function recordValues(nodeList) {
2381 // "Let values be a list of (node, command, specified command value)
2382 // triples, initially empty."
2383 var values = [];
2385 // "For each node in node list, for each command in the list "subscript",
2386 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
2387 // "strikethrough", and "underline" in that order:"
2388 nodeList.forEach(function(node) {
2389 ["subscript", "bold", "fontname", "fontsize", "forecolor",
2390 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) {
2391 // "Let ancestor equal node."
2392 var ancestor = node;
2394 // "If ancestor is not an Element, set it to its parent."
2395 if (ancestor.nodeType != Node.ELEMENT_NODE) {
2396 ancestor = ancestor.parentNode;
2397 }
2399 // "While ancestor is an Element and its specified command value
2400 // for command is null, set it to its parent."
2401 while (ancestor
2402 && ancestor.nodeType == Node.ELEMENT_NODE
2403 && getSpecifiedCommandValue(ancestor, command) === null) {
2404 ancestor = ancestor.parentNode;
2405 }
2407 // "If ancestor is an Element, add (node, command, ancestor's
2408 // specified command value for command) to values. Otherwise add
2409 // (node, command, null) to values."
2410 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
2411 values.push([node, command, getSpecifiedCommandValue(ancestor, command)]);
2412 } else {
2413 values.push([node, command, null]);
2414 }
2415 });
2416 });
2418 // "Return values."
2419 return values;
2420 }
2422 function restoreValues(values) {
2423 // "For each (node, command, value) triple in values:"
2424 values.forEach(function(triple) {
2425 var node = triple[0];
2426 var command = triple[1];
2427 var value = triple[2];
2429 // "Let ancestor equal node."
2430 var ancestor = node;
2432 // "If ancestor is not an Element, set it to its parent."
2433 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
2434 ancestor = ancestor.parentNode;
2435 }
2437 // "While ancestor is an Element and its specified command value for
2438 // command is null, set it to its parent."
2439 while (ancestor
2440 && ancestor.nodeType == Node.ELEMENT_NODE
2441 && getSpecifiedCommandValue(ancestor, command) === null) {
2442 ancestor = ancestor.parentNode;
2443 }
2445 // "If value is null and ancestor is an Element, push down values on
2446 // node for command, with new value null."
2447 if (value === null
2448 && ancestor
2449 && ancestor.nodeType == Node.ELEMENT_NODE) {
2450 pushDownValues(node, command, null);
2452 // "Otherwise, if ancestor is an Element and its specified command
2453 // value for command is not equivalent to value, or if ancestor is not
2454 // an Element and value is not null, force the value of command to
2455 // value on node."
2456 } else if ((ancestor
2457 && ancestor.nodeType == Node.ELEMENT_NODE
2458 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
2459 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
2460 && value !== null)) {
2461 forceValue(node, command, value);
2462 }
2463 });
2464 }
2467 //@}
2468 ///// Clearing an element's value /////
2469 //@{
2471 function clearValue(element, command) {
2472 // "If element is not editable, return the empty list."
2473 if (!isEditable(element)) {
2474 return [];
2475 }
2477 // "If element's specified command value for command is null, return the
2478 // empty list."
2479 if (getSpecifiedCommandValue(element, command) === null) {
2480 return [];
2481 }
2483 // "If element is a simple modifiable element:"
2484 if (isSimpleModifiableElement(element)) {
2485 // "Let children be the children of element."
2486 var children = Array.prototype.slice.call(element.childNodes);
2488 // "For each child in children, insert child into element's parent
2489 // immediately before element, preserving ranges."
2490 for (var i = 0; i < children.length; i++) {
2491 movePreservingRanges(children[i], element.parentNode, getNodeIndex(element));
2492 }
2494 // "Remove element from its parent."
2495 element.parentNode.removeChild(element);
2497 // "Return children."
2498 return children;
2499 }
2501 // "If command is "strikethrough", and element has a style attribute that
2502 // sets "text-decoration" to some value containing "line-through", delete
2503 // "line-through" from the value."
2504 if (command == "strikethrough"
2505 && element.style.textDecoration.indexOf("line-through") != -1) {
2506 if (element.style.textDecoration == "line-through") {
2507 element.style.textDecoration = "";
2508 } else {
2509 element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
2510 }
2511 if (element.getAttribute("style") == "") {
2512 element.removeAttribute("style");
2513 }
2514 }
2516 // "If command is "underline", and element has a style attribute that sets
2517 // "text-decoration" to some value containing "underline", delete
2518 // "underline" from the value."
2519 if (command == "underline"
2520 && element.style.textDecoration.indexOf("underline") != -1) {
2521 if (element.style.textDecoration == "underline") {
2522 element.style.textDecoration = "";
2523 } else {
2524 element.style.textDecoration = element.style.textDecoration.replace("underline", "");
2525 }
2526 if (element.getAttribute("style") == "") {
2527 element.removeAttribute("style");
2528 }
2529 }
2531 // "If the relevant CSS property for command is not null, unset the CSS
2532 // property property of element."
2533 if (commands[command].relevantCssProperty !== null) {
2534 element.style[commands[command].relevantCssProperty] = '';
2535 if (element.getAttribute("style") == "") {
2536 element.removeAttribute("style");
2537 }
2538 }
2540 // "If element is a font element:"
2541 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
2542 // "If command is "foreColor", unset element's color attribute, if set."
2543 if (command == "forecolor") {
2544 element.removeAttribute("color");
2545 }
2547 // "If command is "fontName", unset element's face attribute, if set."
2548 if (command == "fontname") {
2549 element.removeAttribute("face");
2550 }
2552 // "If command is "fontSize", unset element's size attribute, if set."
2553 if (command == "fontsize") {
2554 element.removeAttribute("size");
2555 }
2556 }
2558 // "If element is an a element and command is "createLink" or "unlink",
2559 // unset the href property of element."
2560 if (isHtmlElement(element, "A")
2561 && (command == "createlink" || command == "unlink")) {
2562 element.removeAttribute("href");
2563 }
2565 // "If element's specified command value for command is null, return the
2566 // empty list."
2567 if (getSpecifiedCommandValue(element, command) === null) {
2568 return [];
2569 }
2571 // "Set the tag name of element to "span", and return the one-node list
2572 // consisting of the result."
2573 return [setTagName(element, "span")];
2574 }
2577 //@}
2578 ///// Pushing down values /////
2579 //@{
2581 function pushDownValues(node, command, newValue) {
2582 // "If node's parent is not an Element, abort this algorithm."
2583 if (!node.parentNode
2584 || node.parentNode.nodeType != Node.ELEMENT_NODE) {
2585 return;
2586 }
2588 // "If the effective command value of command is loosely equivalent to new
2589 // value on node, abort this algorithm."
2590 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2591 return;
2592 }
2594 // "Let current ancestor be node's parent."
2595 var currentAncestor = node.parentNode;
2597 // "Let ancestor list be a list of Nodes, initially empty."
2598 var ancestorList = [];
2600 // "While current ancestor is an editable Element and the effective command
2601 // value of command is not loosely equivalent to new value on it, append
2602 // current ancestor to ancestor list, then set current ancestor to its
2603 // parent."
2604 while (isEditable(currentAncestor)
2605 && currentAncestor.nodeType == Node.ELEMENT_NODE
2606 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
2607 ancestorList.push(currentAncestor);
2608 currentAncestor = currentAncestor.parentNode;
2609 }
2611 // "If ancestor list is empty, abort this algorithm."
2612 if (!ancestorList.length) {
2613 return;
2614 }
2616 // "Let propagated value be the specified command value of command on the
2617 // last member of ancestor list."
2618 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
2620 // "If propagated value is null and is not equal to new value, abort this
2621 // algorithm."
2622 if (propagatedValue === null && propagatedValue != newValue) {
2623 return;
2624 }
2626 // "If the effective command value for the parent of the last member of
2627 // ancestor list is not loosely equivalent to new value, and new value is
2628 // not null, abort this algorithm."
2629 if (newValue !== null
2630 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
2631 return;
2632 }
2634 // "While ancestor list is not empty:"
2635 while (ancestorList.length) {
2636 // "Let current ancestor be the last member of ancestor list."
2637 // "Remove the last member from ancestor list."
2638 var currentAncestor = ancestorList.pop();
2640 // "If the specified command value of current ancestor for command is
2641 // not null, set propagated value to that value."
2642 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2643 propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
2644 }
2646 // "Let children be the children of current ancestor."
2647 var children = Array.prototype.slice.call(currentAncestor.childNodes);
2649 // "If the specified command value of current ancestor for command is
2650 // not null, clear the value of current ancestor."
2651 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2652 clearValue(currentAncestor, command);
2653 }
2655 // "For every child in children:"
2656 for (var i = 0; i < children.length; i++) {
2657 var child = children[i];
2659 // "If child is node, continue with the next child."
2660 if (child == node) {
2661 continue;
2662 }
2664 // "If child is an Element whose specified command value for
2665 // command is neither null nor equivalent to propagated value,
2666 // continue with the next child."
2667 if (child.nodeType == Node.ELEMENT_NODE
2668 && getSpecifiedCommandValue(child, command) !== null
2669 && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
2670 continue;
2671 }
2673 // "If child is the last member of ancestor list, continue with the
2674 // next child."
2675 if (child == ancestorList[ancestorList.length - 1]) {
2676 continue;
2677 }
2679 // "Force the value of child, with command as in this algorithm
2680 // and new value equal to propagated value."
2681 forceValue(child, command, propagatedValue);
2682 }
2683 }
2684 }
2687 //@}
2688 ///// Forcing the value of a node /////
2689 //@{
2691 function forceValue(node, command, newValue) {
2692 // "If node's parent is null, abort this algorithm."
2693 if (!node.parentNode) {
2694 return;
2695 }
2697 // "If new value is null, abort this algorithm."
2698 if (newValue === null) {
2699 return;
2700 }
2702 // "If node is an allowed child of "span":"
2703 if (isAllowedChild(node, "span")) {
2704 // "Reorder modifiable descendants of node's previousSibling."
2705 reorderModifiableDescendants(node.previousSibling, command, newValue);
2707 // "Reorder modifiable descendants of node's nextSibling."
2708 reorderModifiableDescendants(node.nextSibling, command, newValue);
2710 // "Wrap the one-node list consisting of node, with sibling criteria
2711 // returning true for a simple modifiable element whose specified
2712 // command value is equivalent to new value and whose effective command
2713 // value is loosely equivalent to new value and false otherwise, and
2714 // with new parent instructions returning null."
2715 wrap([node],
2716 function(node) {
2717 return isSimpleModifiableElement(node)
2718 && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
2719 && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
2720 },
2721 function() { return null }
2722 );
2723 }
2725 // "If node is invisible, abort this algorithm."
2726 if (isInvisible(node)) {
2727 return;
2728 }
2730 // "If the effective command value of command is loosely equivalent to new
2731 // value on node, abort this algorithm."
2732 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2733 return;
2734 }
2736 // "If node is not an allowed child of "span":"
2737 if (!isAllowedChild(node, "span")) {
2738 // "Let children be all children of node, omitting any that are
2739 // Elements whose specified command value for command is neither null
2740 // nor equivalent to new value."
2741 var children = [];
2742 for (var i = 0; i < node.childNodes.length; i++) {
2743 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2744 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
2746 if (specifiedValue !== null
2747 && !areEquivalentValues(command, newValue, specifiedValue)) {
2748 continue;
2749 }
2750 }
2751 children.push(node.childNodes[i]);
2752 }
2754 // "Force the value of each Node in children, with command and new
2755 // value as in this invocation of the algorithm."
2756 for (var i = 0; i < children.length; i++) {
2757 forceValue(children[i], command, newValue);
2758 }
2760 // "Abort this algorithm."
2761 return;
2762 }
2764 // "If the effective command value of command is loosely equivalent to new
2765 // value on node, abort this algorithm."
2766 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2767 return;
2768 }
2770 // "Let new parent be null."
2771 var newParent = null;
2773 // "If the CSS styling flag is false:"
2774 if (!cssStylingFlag) {
2775 // "If command is "bold" and new value is "bold", let new parent be the
2776 // result of calling createElement("b") on the ownerDocument of node."
2777 if (command == "bold" && (newValue == "bold" || newValue == "700")) {
2778 newParent = node.ownerDocument.createElement("b");
2779 }
2781 // "If command is "italic" and new value is "italic", let new parent be
2782 // the result of calling createElement("i") on the ownerDocument of
2783 // node."
2784 if (command == "italic" && newValue == "italic") {
2785 newParent = node.ownerDocument.createElement("i");
2786 }
2788 // "If command is "strikethrough" and new value is "line-through", let
2789 // new parent be the result of calling createElement("s") on the
2790 // ownerDocument of node."
2791 if (command == "strikethrough" && newValue == "line-through") {
2792 newParent = node.ownerDocument.createElement("s");
2793 }
2795 // "If command is "underline" and new value is "underline", let new
2796 // parent be the result of calling createElement("u") on the
2797 // ownerDocument of node."
2798 if (command == "underline" && newValue == "underline") {
2799 newParent = node.ownerDocument.createElement("u");
2800 }
2802 // "If command is "foreColor", and new value is fully opaque with red,
2803 // green, and blue components in the range 0 to 255:"
2804 if (command == "forecolor" && parseSimpleColor(newValue)) {
2805 // "Let new parent be the result of calling createElement("font")
2806 // on the ownerDocument of node."
2807 newParent = node.ownerDocument.createElement("font");
2809 // "Set the color attribute of new parent to the result of applying
2810 // the rules for serializing simple color values to new value
2811 // (interpreted as a simple color)."
2812 newParent.setAttribute("color", parseSimpleColor(newValue));
2813 }
2815 // "If command is "fontName", let new parent be the result of calling
2816 // createElement("font") on the ownerDocument of node, then set the
2817 // face attribute of new parent to new value."
2818 if (command == "fontname") {
2819 newParent = node.ownerDocument.createElement("font");
2820 newParent.face = newValue;
2821 }
2822 }
2824 // "If command is "createLink" or "unlink":"
2825 if (command == "createlink" || command == "unlink") {
2826 // "Let new parent be the result of calling createElement("a") on the
2827 // ownerDocument of node."
2828 newParent = node.ownerDocument.createElement("a");
2830 // "Set the href attribute of new parent to new value."
2831 newParent.setAttribute("href", newValue);
2833 // "Let ancestor be node's parent."
2834 var ancestor = node.parentNode;
2836 // "While ancestor is not null:"
2837 while (ancestor) {
2838 // "If ancestor is an a, set the tag name of ancestor to "span",
2839 // and let ancestor be the result."
2840 if (isHtmlElement(ancestor, "A")) {
2841 ancestor = setTagName(ancestor, "span");
2842 }
2844 // "Set ancestor to its parent."
2845 ancestor = ancestor.parentNode;
2846 }
2847 }
2849 // "If command is "fontSize"; and new value is one of "x-small", "small",
2850 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
2851 // CSS styling flag is false, or new value is "xxx-large": let new parent
2852 // be the result of calling createElement("font") on the ownerDocument of
2853 // node, then set the size attribute of new parent to the number from the
2854 // following table based on new value: [table omitted]"
2855 if (command == "fontsize"
2856 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
2857 && (!cssStylingFlag || newValue == "xxx-large")) {
2858 newParent = node.ownerDocument.createElement("font");
2859 newParent.size = cssSizeToLegacy(newValue);
2860 }
2862 // "If command is "subscript" or "superscript" and new value is
2863 // "subscript", let new parent be the result of calling
2864 // createElement("sub") on the ownerDocument of node."
2865 if ((command == "subscript" || command == "superscript")
2866 && newValue == "subscript") {
2867 newParent = node.ownerDocument.createElement("sub");
2868 }
2870 // "If command is "subscript" or "superscript" and new value is
2871 // "superscript", let new parent be the result of calling
2872 // createElement("sup") on the ownerDocument of node."
2873 if ((command == "subscript" || command == "superscript")
2874 && newValue == "superscript") {
2875 newParent = node.ownerDocument.createElement("sup");
2876 }
2878 // "If new parent is null, let new parent be the result of calling
2879 // createElement("span") on the ownerDocument of node."
2880 if (!newParent) {
2881 newParent = node.ownerDocument.createElement("span");
2882 }
2884 // "Insert new parent in node's parent before node."
2885 node.parentNode.insertBefore(newParent, node);
2887 // "If the effective command value of command for new parent is not loosely
2888 // equivalent to new value, and the relevant CSS property for command is
2889 // not null, set that CSS property of new parent to new value (if the new
2890 // value would be valid)."
2891 var property = commands[command].relevantCssProperty;
2892 if (property !== null
2893 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
2894 newParent.style[property] = newValue;
2895 }
2897 // "If command is "strikethrough", and new value is "line-through", and the
2898 // effective command value of "strikethrough" for new parent is not
2899 // "line-through", set the "text-decoration" property of new parent to
2900 // "line-through"."
2901 if (command == "strikethrough"
2902 && newValue == "line-through"
2903 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
2904 newParent.style.textDecoration = "line-through";
2905 }
2907 // "If command is "underline", and new value is "underline", and the
2908 // effective command value of "underline" for new parent is not
2909 // "underline", set the "text-decoration" property of new parent to
2910 // "underline"."
2911 if (command == "underline"
2912 && newValue == "underline"
2913 && getEffectiveCommandValue(newParent, "underline") != "underline") {
2914 newParent.style.textDecoration = "underline";
2915 }
2917 // "Append node to new parent as its last child, preserving ranges."
2918 movePreservingRanges(node, newParent, newParent.childNodes.length);
2920 // "If node is an Element and the effective command value of command for
2921 // node is not loosely equivalent to new value:"
2922 if (node.nodeType == Node.ELEMENT_NODE
2923 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2924 // "Insert node into the parent of new parent before new parent,
2925 // preserving ranges."
2926 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
2928 // "Remove new parent from its parent."
2929 newParent.parentNode.removeChild(newParent);
2931 // "Let children be all children of node, omitting any that are
2932 // Elements whose specified command value for command is neither null
2933 // nor equivalent to new value."
2934 var children = [];
2935 for (var i = 0; i < node.childNodes.length; i++) {
2936 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2937 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
2939 if (specifiedValue !== null
2940 && !areEquivalentValues(command, newValue, specifiedValue)) {
2941 continue;
2942 }
2943 }
2944 children.push(node.childNodes[i]);
2945 }
2947 // "Force the value of each Node in children, with command and new
2948 // value as in this invocation of the algorithm."
2949 for (var i = 0; i < children.length; i++) {
2950 forceValue(children[i], command, newValue);
2951 }
2952 }
2953 }
2956 //@}
2957 ///// Setting the selection's value /////
2958 //@{
2960 function setSelectionValue(command, newValue) {
2961 // "If there is no formattable node effectively contained in the active
2962 // range:"
2963 if (!getAllEffectivelyContainedNodes(getActiveRange())
2964 .some(isFormattableNode)) {
2965 // "If command has inline command activated values, set the state
2966 // override to true if new value is among them and false if it's not."
2967 if ("inlineCommandActivatedValues" in commands[command]) {
2968 setStateOverride(command, commands[command].inlineCommandActivatedValues
2969 .indexOf(newValue) != -1);
2970 }
2972 // "If command is "subscript", unset the state override for
2973 // "superscript"."
2974 if (command == "subscript") {
2975 unsetStateOverride("superscript");
2976 }
2978 // "If command is "superscript", unset the state override for
2979 // "subscript"."
2980 if (command == "superscript") {
2981 unsetStateOverride("subscript");
2982 }
2984 // "If new value is null, unset the value override (if any)."
2985 if (newValue === null) {
2986 unsetValueOverride(command);
2988 // "Otherwise, if command is "createLink" or it has a value specified,
2989 // set the value override to new value."
2990 } else if (command == "createlink" || "value" in commands[command]) {
2991 setValueOverride(command, newValue);
2992 }
2994 // "Abort these steps."
2995 return;
2996 }
2998 // "If the active range's start node is an editable Text node, and its
2999 // start offset is neither zero nor its start node's length, call
3000 // splitText() on the active range's start node, with argument equal to the
3001 // active range's start offset. Then set the active range's start node to
3002 // the result, and its start offset to zero."
3003 if (isEditable(getActiveRange().startContainer)
3004 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3005 && getActiveRange().startOffset != 0
3006 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
3007 // Account for browsers not following range mutation rules
3008 var newActiveRange = document.createRange();
3009 var newNode;
3010 if (getActiveRange().startContainer == getActiveRange().endContainer) {
3011 var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset;
3012 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3013 newActiveRange.setEnd(newNode, newEndOffset);
3014 getActiveRange().setEnd(newNode, newEndOffset);
3015 } else {
3016 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3017 }
3018 newActiveRange.setStart(newNode, 0);
3019 getSelection().removeAllRanges();
3020 getSelection().addRange(newActiveRange);
3022 getActiveRange().setStart(newNode, 0);
3023 }
3025 // "If the active range's end node is an editable Text node, and its end
3026 // offset is neither zero nor its end node's length, call splitText() on
3027 // the active range's end node, with argument equal to the active range's
3028 // end offset."
3029 if (isEditable(getActiveRange().endContainer)
3030 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3031 && getActiveRange().endOffset != 0
3032 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
3033 // IE seems to mutate the range incorrectly here, so we need correction
3034 // here as well. The active range will be temporarily in orphaned
3035 // nodes, so calling getActiveRange() after splitText() but before
3036 // fixing the range will throw an exception.
3037 var activeRange = getActiveRange();
3038 var newStart = [activeRange.startContainer, activeRange.startOffset];
3039 var newEnd = [activeRange.endContainer, activeRange.endOffset];
3040 activeRange.endContainer.splitText(activeRange.endOffset);
3041 activeRange.setStart(newStart[0], newStart[1]);
3042 activeRange.setEnd(newEnd[0], newEnd[1]);
3044 getSelection().removeAllRanges();
3045 getSelection().addRange(activeRange);
3046 }
3048 // "Let element list be all editable Elements effectively contained in the
3049 // active range.
3050 //
3051 // "For each element in element list, clear the value of element."
3052 getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3053 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
3054 }).forEach(function(element) {
3055 clearValue(element, command);
3056 });
3058 // "Let node list be all editable nodes effectively contained in the active
3059 // range.
3060 //
3061 // "For each node in node list:"
3062 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
3063 // "Push down values on node."
3064 pushDownValues(node, command, newValue);
3066 // "If node is an allowed child of span, force the value of node."
3067 if (isAllowedChild(node, "span")) {
3068 forceValue(node, command, newValue);
3069 }
3070 });
3071 }
3074 //@}
3075 ///// The backColor command /////
3076 //@{
3077 commands.backcolor = {
3078 // Copy-pasted, same as hiliteColor
3079 action: function(value) {
3080 // Action is further copy-pasted, same as foreColor
3082 // "If value is not a valid CSS color, prepend "#" to it."
3083 //
3084 // "If value is still not a valid CSS color, or if it is currentColor,
3085 // return false."
3086 //
3087 // Cheap hack for testing, no attempt to be comprehensive.
3088 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3089 value = "#" + value;
3090 }
3091 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3092 && !parseSimpleColor(value)
3093 && value.toLowerCase() != "transparent") {
3094 return false;
3095 }
3097 // "Set the selection's value to value."
3098 setSelectionValue("backcolor", value);
3100 // "Return true."
3101 return true;
3102 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3103 equivalentValues: function(val1, val2) {
3104 // "Either both strings are valid CSS colors and have the same red,
3105 // green, blue, and alpha components, or neither string is a valid CSS
3106 // color."
3107 return normalizeColor(val1) === normalizeColor(val2);
3108 },
3109 };
3111 //@}
3112 ///// The bold command /////
3113 //@{
3114 commands.bold = {
3115 action: function() {
3116 // "If queryCommandState("bold") returns true, set the selection's
3117 // value to "normal". Otherwise set the selection's value to "bold".
3118 // Either way, return true."
3119 if (myQueryCommandState("bold")) {
3120 setSelectionValue("bold", "normal");
3121 } else {
3122 setSelectionValue("bold", "bold");
3123 }
3124 return true;
3125 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
3126 relevantCssProperty: "fontWeight",
3127 equivalentValues: function(val1, val2) {
3128 // "Either the two strings are equal, or one is "bold" and the other is
3129 // "700", or one is "normal" and the other is "400"."
3130 return val1 == val2
3131 || (val1 == "bold" && val2 == "700")
3132 || (val1 == "700" && val2 == "bold")
3133 || (val1 == "normal" && val2 == "400")
3134 || (val1 == "400" && val2 == "normal");
3135 },
3136 };
3138 //@}
3139 ///// The createLink command /////
3140 //@{
3141 commands.createlink = {
3142 action: function(value) {
3143 // "If value is the empty string, return false."
3144 if (value === "") {
3145 return false;
3146 }
3148 // "For each editable a element that has an href attribute and is an
3149 // ancestor of some node effectively contained in the active range, set
3150 // that a element's href attribute to value."
3151 //
3152 // TODO: We don't actually do this in tree order, not that it matters
3153 // unless you're spying with mutation events.
3154 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
3155 getAncestors(node).forEach(function(ancestor) {
3156 if (isEditable(ancestor)
3157 && isHtmlElement(ancestor, "a")
3158 && ancestor.hasAttribute("href")) {
3159 ancestor.setAttribute("href", value);
3160 }
3161 });
3162 });
3164 // "Set the selection's value to value."
3165 setSelectionValue("createlink", value);
3167 // "Return true."
3168 return true;
3169 }
3170 };
3172 //@}
3173 ///// The fontName command /////
3174 //@{
3175 commands.fontname = {
3176 action: function(value) {
3177 // "Set the selection's value to value, then return true."
3178 setSelectionValue("fontname", value);
3179 return true;
3180 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
3181 };
3183 //@}
3184 ///// The fontSize command /////
3185 //@{
3187 // Helper function for fontSize's action plus queryOutputHelper. It's just the
3188 // middle of fontSize's action, ripped out into its own function. Returns null
3189 // if the size is invalid.
3190 function normalizeFontSize(value) {
3191 // "Strip leading and trailing whitespace from value."
3192 //
3193 // Cheap hack, not following the actual algorithm.
3194 value = value.trim();
3196 // "If value is not a valid floating point number, and would not be a valid
3197 // floating point number if a single leading "+" character were stripped,
3198 // return false."
3199 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
3200 return null;
3201 }
3203 var mode;
3205 // "If the first character of value is "+", delete the character and let
3206 // mode be "relative-plus"."
3207 if (value[0] == "+") {
3208 value = value.slice(1);
3209 mode = "relative-plus";
3210 // "Otherwise, if the first character of value is "-", delete the character
3211 // and let mode be "relative-minus"."
3212 } else if (value[0] == "-") {
3213 value = value.slice(1);
3214 mode = "relative-minus";
3215 // "Otherwise, let mode be "absolute"."
3216 } else {
3217 mode = "absolute";
3218 }
3220 // "Apply the rules for parsing non-negative integers to value, and let
3221 // number be the result."
3222 //
3223 // Another cheap hack.
3224 var num = parseInt(value);
3226 // "If mode is "relative-plus", add three to number."
3227 if (mode == "relative-plus") {
3228 num += 3;
3229 }
3231 // "If mode is "relative-minus", negate number, then add three to it."
3232 if (mode == "relative-minus") {
3233 num = 3 - num;
3234 }
3236 // "If number is less than one, let number equal 1."
3237 if (num < 1) {
3238 num = 1;
3239 }
3241 // "If number is greater than seven, let number equal 7."
3242 if (num > 7) {
3243 num = 7;
3244 }
3246 // "Set value to the string here corresponding to number:" [table omitted]
3247 value = {
3248 1: "x-small",
3249 2: "small",
3250 3: "medium",
3251 4: "large",
3252 5: "x-large",
3253 6: "xx-large",
3254 7: "xxx-large"
3255 }[num];
3257 return value;
3258 }
3260 commands.fontsize = {
3261 action: function(value) {
3262 value = normalizeFontSize(value);
3263 if (value === null) {
3264 return false;
3265 }
3267 // "Set the selection's value to value."
3268 setSelectionValue("fontsize", value);
3270 // "Return true."
3271 return true;
3272 }, indeterm: function() {
3273 // "True if among formattable nodes that are effectively contained in
3274 // the active range, there are two that have distinct effective command
3275 // values. Otherwise false."
3276 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
3277 .map(function(node) {
3278 return getEffectiveCommandValue(node, "fontsize");
3279 }).filter(function(value, i, arr) {
3280 return arr.slice(0, i).indexOf(value) == -1;
3281 }).length >= 2;
3282 }, value: function() {
3283 // "If the active range is null, return the empty string."
3284 if (!getActiveRange()) {
3285 return "";
3286 }
3288 // "Let pixel size be the effective command value of the first
3289 // formattable node that is effectively contained in the active range,
3290 // or if there is no such node, the effective command value of the
3291 // active range's start node, in either case interpreted as a number of
3292 // pixels."
3293 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
3294 if (node === undefined) {
3295 node = getActiveRange().startContainer;
3296 }
3297 var pixelSize = getEffectiveCommandValue(node, "fontsize");
3299 // "Return the legacy font size for pixel size."
3300 return getLegacyFontSize(pixelSize);
3301 }, relevantCssProperty: "fontSize"
3302 };
3304 function getLegacyFontSize(size) {
3305 if (getLegacyFontSize.resultCache === undefined) {
3306 getLegacyFontSize.resultCache = {};
3307 }
3309 if (getLegacyFontSize.resultCache[size] !== undefined) {
3310 return getLegacyFontSize.resultCache[size];
3311 }
3313 // For convenience in other places in my code, I handle all sizes, not just
3314 // pixel sizes as the spec says. This means pixel sizes have to be passed
3315 // in suffixed with "px", not as plain numbers.
3316 if (normalizeFontSize(size) !== null) {
3317 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size));
3318 }
3320 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1
3321 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
3322 // There is no sensible legacy size for things like "2em".
3323 return getLegacyFontSize.resultCache[size] = null;
3324 }
3326 var font = document.createElement("font");
3327 document.body.appendChild(font);
3328 if (size == "xxx-large") {
3329 font.size = 7;
3330 } else {
3331 font.style.fontSize = size;
3332 }
3333 var pixelSize = parseInt(getComputedStyle(font).fontSize);
3334 document.body.removeChild(font);
3336 // "Let returned size be 1."
3337 var returnedSize = 1;
3339 // "While returned size is less than 7:"
3340 while (returnedSize < 7) {
3341 // "Let lower bound be the resolved value of "font-size" in pixels
3342 // of a font element whose size attribute is set to returned size."
3343 var font = document.createElement("font");
3344 font.size = returnedSize;
3345 document.body.appendChild(font);
3346 var lowerBound = parseInt(getComputedStyle(font).fontSize);
3348 // "Let upper bound be the resolved value of "font-size" in pixels
3349 // of a font element whose size attribute is set to one plus
3350 // returned size."
3351 font.size = 1 + returnedSize;
3352 var upperBound = parseInt(getComputedStyle(font).fontSize);
3353 document.body.removeChild(font);
3355 // "Let average be the average of upper bound and lower bound."
3356 var average = (upperBound + lowerBound)/2;
3358 // "If pixel size is less than average, return the one-element
3359 // string consisting of the digit returned size."
3360 if (pixelSize < average) {
3361 return getLegacyFontSize.resultCache[size] = String(returnedSize);
3362 }
3364 // "Add one to returned size."
3365 returnedSize++;
3366 }
3368 // "Return "7"."
3369 return getLegacyFontSize.resultCache[size] = "7";
3370 }
3372 //@}
3373 ///// The foreColor command /////
3374 //@{
3375 commands.forecolor = {
3376 action: function(value) {
3377 // Copy-pasted, same as backColor and hiliteColor
3379 // "If value is not a valid CSS color, prepend "#" to it."
3380 //
3381 // "If value is still not a valid CSS color, or if it is currentColor,
3382 // return false."
3383 //
3384 // Cheap hack for testing, no attempt to be comprehensive.
3385 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3386 value = "#" + value;
3387 }
3388 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3389 && !parseSimpleColor(value)
3390 && value.toLowerCase() != "transparent") {
3391 return false;
3392 }
3394 // "Set the selection's value to value."
3395 setSelectionValue("forecolor", value);
3397 // "Return true."
3398 return true;
3399 }, standardInlineValueCommand: true, relevantCssProperty: "color",
3400 equivalentValues: function(val1, val2) {
3401 // "Either both strings are valid CSS colors and have the same red,
3402 // green, blue, and alpha components, or neither string is a valid CSS
3403 // color."
3404 return normalizeColor(val1) === normalizeColor(val2);
3405 },
3406 };
3408 //@}
3409 ///// The hiliteColor command /////
3410 //@{
3411 commands.hilitecolor = {
3412 // Copy-pasted, same as backColor
3413 action: function(value) {
3414 // Action is further copy-pasted, same as foreColor
3416 // "If value is not a valid CSS color, prepend "#" to it."
3417 //
3418 // "If value is still not a valid CSS color, or if it is currentColor,
3419 // return false."
3420 //
3421 // Cheap hack for testing, no attempt to be comprehensive.
3422 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3423 value = "#" + value;
3424 }
3425 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3426 && !parseSimpleColor(value)
3427 && value.toLowerCase() != "transparent") {
3428 return false;
3429 }
3431 // "Set the selection's value to value."
3432 setSelectionValue("hilitecolor", value);
3434 // "Return true."
3435 return true;
3436 }, indeterm: function() {
3437 // "True if among editable Text nodes that are effectively contained in
3438 // the active range, there are two that have distinct effective command
3439 // values. Otherwise false."
3440 return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3441 return isEditable(node) && node.nodeType == Node.TEXT_NODE;
3442 }).map(function(node) {
3443 return getEffectiveCommandValue(node, "hilitecolor");
3444 }).filter(function(value, i, arr) {
3445 return arr.slice(0, i).indexOf(value) == -1;
3446 }).length >= 2;
3447 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3448 equivalentValues: function(val1, val2) {
3449 // "Either both strings are valid CSS colors and have the same red,
3450 // green, blue, and alpha components, or neither string is a valid CSS
3451 // color."
3452 return normalizeColor(val1) === normalizeColor(val2);
3453 },
3454 };
3456 //@}
3457 ///// The italic command /////
3458 //@{
3459 commands.italic = {
3460 action: function() {
3461 // "If queryCommandState("italic") returns true, set the selection's
3462 // value to "normal". Otherwise set the selection's value to "italic".
3463 // Either way, return true."
3464 if (myQueryCommandState("italic")) {
3465 setSelectionValue("italic", "normal");
3466 } else {
3467 setSelectionValue("italic", "italic");
3468 }
3469 return true;
3470 }, inlineCommandActivatedValues: ["italic", "oblique"],
3471 relevantCssProperty: "fontStyle"
3472 };
3474 //@}
3475 ///// The removeFormat command /////
3476 //@{
3477 commands.removeformat = {
3478 action: function() {
3479 // "A removeFormat candidate is an editable HTML element with local
3480 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
3481 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
3482 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
3483 // "tt", "u", or "var"."
3484 function isRemoveFormatCandidate(node) {
3485 return isEditable(node)
3486 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
3487 "big", "blink", "cite", "code", "dfn", "em", "font", "i",
3488 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
3489 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
3490 }
3492 // "Let elements to remove be a list of every removeFormat candidate
3493 // effectively contained in the active range."
3494 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
3496 // "For each element in elements to remove:"
3497 elementsToRemove.forEach(function(element) {
3498 // "While element has children, insert the first child of element
3499 // into the parent of element immediately before element,
3500 // preserving ranges."
3501 while (element.hasChildNodes()) {
3502 movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
3503 }
3505 // "Remove element from its parent."
3506 element.parentNode.removeChild(element);
3507 });
3509 // "If the active range's start node is an editable Text node, and its
3510 // start offset is neither zero nor its start node's length, call
3511 // splitText() on the active range's start node, with argument equal to
3512 // the active range's start offset. Then set the active range's start
3513 // node to the result, and its start offset to zero."
3514 if (isEditable(getActiveRange().startContainer)
3515 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3516 && getActiveRange().startOffset != 0
3517 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
3518 // Account for browsers not following range mutation rules
3519 if (getActiveRange().startContainer == getActiveRange().endContainer) {
3520 var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
3521 var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3522 getActiveRange().setStart(newNode, 0);
3523 getActiveRange().setEnd(newNode, newEnd);
3524 } else {
3525 getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
3526 }
3527 }
3529 // "If the active range's end node is an editable Text node, and its
3530 // end offset is neither zero nor its end node's length, call
3531 // splitText() on the active range's end node, with argument equal to
3532 // the active range's end offset."
3533 if (isEditable(getActiveRange().endContainer)
3534 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3535 && getActiveRange().endOffset != 0
3536 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
3537 // IE seems to mutate the range incorrectly here, so we need
3538 // correction here as well. Have to be careful to set the range to
3539 // something not including the text node so that getActiveRange()
3540 // doesn't throw an exception due to a temporarily detached
3541 // endpoint.
3542 var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
3543 var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
3544 getActiveRange().setEnd(document.documentElement, 0);
3545 newEnd[0].splitText(newEnd[1]);
3546 getActiveRange().setStart(newStart[0], newStart[1]);
3547 getActiveRange().setEnd(newEnd[0], newEnd[1]);
3548 }
3550 // "Let node list consist of all editable nodes effectively contained
3551 // in the active range."
3552 //
3553 // "For each node in node list, while node's parent is a removeFormat
3554 // candidate in the same editing host as node, split the parent of the
3555 // one-node list consisting of node."
3556 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
3557 while (isRemoveFormatCandidate(node.parentNode)
3558 && inSameEditingHost(node.parentNode, node)) {
3559 splitParent([node]);
3560 }
3561 });
3563 // "For each of the entries in the following list, in the given order,
3564 // set the selection's value to null, with command as given."
3565 [
3566 "subscript",
3567 "bold",
3568 "fontname",
3569 "fontsize",
3570 "forecolor",
3571 "hilitecolor",
3572 "italic",
3573 "strikethrough",
3574 "underline",
3575 ].forEach(function(command) {
3576 setSelectionValue(command, null);
3577 });
3579 // "Return true."
3580 return true;
3581 }
3582 };
3584 //@}
3585 ///// The strikethrough command /////
3586 //@{
3587 commands.strikethrough = {
3588 action: function() {
3589 // "If queryCommandState("strikethrough") returns true, set the
3590 // selection's value to null. Otherwise set the selection's value to
3591 // "line-through". Either way, return true."
3592 if (myQueryCommandState("strikethrough")) {
3593 setSelectionValue("strikethrough", null);
3594 } else {
3595 setSelectionValue("strikethrough", "line-through");
3596 }
3597 return true;
3598 }, inlineCommandActivatedValues: ["line-through"]
3599 };
3601 //@}
3602 ///// The subscript command /////
3603 //@{
3604 commands.subscript = {
3605 action: function() {
3606 // "Call queryCommandState("subscript"), and let state be the result."
3607 var state = myQueryCommandState("subscript");
3609 // "Set the selection's value to null."
3610 setSelectionValue("subscript", null);
3612 // "If state is false, set the selection's value to "subscript"."
3613 if (!state) {
3614 setSelectionValue("subscript", "subscript");
3615 }
3617 // "Return true."
3618 return true;
3619 }, indeterm: function() {
3620 // "True if either among formattable nodes that are effectively
3621 // contained in the active range, there is at least one with effective
3622 // command value "subscript" and at least one with some other effective
3623 // command value; or if there is some formattable node effectively
3624 // contained in the active range with effective command value "mixed".
3625 // Otherwise false."
3626 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
3627 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
3628 && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
3629 || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
3630 }, inlineCommandActivatedValues: ["subscript"],
3631 };
3633 //@}
3634 ///// The superscript command /////
3635 //@{
3636 commands.superscript = {
3637 action: function() {
3638 // "Call queryCommandState("superscript"), and let state be the
3639 // result."
3640 var state = myQueryCommandState("superscript");
3642 // "Set the selection's value to null."
3643 setSelectionValue("superscript", null);
3645 // "If state is false, set the selection's value to "superscript"."
3646 if (!state) {
3647 setSelectionValue("superscript", "superscript");
3648 }
3650 // "Return true."
3651 return true;
3652 }, indeterm: function() {
3653 // "True if either among formattable nodes that are effectively
3654 // contained in the active range, there is at least one with effective
3655 // command value "superscript" and at least one with some other
3656 // effective command value; or if there is some formattable node
3657 // effectively contained in the active range with effective command
3658 // value "mixed". Otherwise false."
3659 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
3660 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
3661 && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
3662 || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
3663 }, inlineCommandActivatedValues: ["superscript"],
3664 };
3666 //@}
3667 ///// The underline command /////
3668 //@{
3669 commands.underline = {
3670 action: function() {
3671 // "If queryCommandState("underline") returns true, set the selection's
3672 // value to null. Otherwise set the selection's value to "underline".
3673 // Either way, return true."
3674 if (myQueryCommandState("underline")) {
3675 setSelectionValue("underline", null);
3676 } else {
3677 setSelectionValue("underline", "underline");
3678 }
3679 return true;
3680 }, inlineCommandActivatedValues: ["underline"]
3681 };
3683 //@}
3684 ///// The unlink command /////
3685 //@{
3686 commands.unlink = {
3687 action: function() {
3688 // "Let hyperlinks be a list of every a element that has an href
3689 // attribute and is contained in the active range or is an ancestor of
3690 // one of its boundary points."
3691 //
3692 // As usual, take care to ensure it's tree order. The correctness of
3693 // the following is left as an exercise for the reader.
3694 var range = getActiveRange();
3695 var hyperlinks = [];
3696 for (
3697 var node = range.startContainer;
3698 node;
3699 node = node.parentNode
3700 ) {
3701 if (isHtmlElement(node, "A")
3702 && node.hasAttribute("href")) {
3703 hyperlinks.unshift(node);
3704 }
3705 }
3706 for (
3707 var node = range.startContainer;
3708 node != nextNodeDescendants(range.endContainer);
3709 node = nextNode(node)
3710 ) {
3711 if (isHtmlElement(node, "A")
3712 && node.hasAttribute("href")
3713 && (isContained(node, range)
3714 || isAncestor(node, range.endContainer)
3715 || node == range.endContainer)) {
3716 hyperlinks.push(node);
3717 }
3718 }
3720 // "Clear the value of each member of hyperlinks."
3721 for (var i = 0; i < hyperlinks.length; i++) {
3722 clearValue(hyperlinks[i], "unlink");
3723 }
3725 // "Return true."
3726 return true;
3727 }
3728 };
3730 //@}
3732 /////////////////////////////////////
3733 ///// Block formatting commands /////
3734 /////////////////////////////////////
3736 ///// Block formatting command definitions /////
3737 //@{
3739 // "An indentation element is either a blockquote, or a div that has a style
3740 // attribute that sets "margin" or some subproperty of it."
3741 function isIndentationElement(node) {
3742 if (!isHtmlElement(node)) {
3743 return false;
3744 }
3746 if (node.tagName == "BLOCKQUOTE") {
3747 return true;
3748 }
3750 if (node.tagName != "DIV") {
3751 return false;
3752 }
3754 for (var i = 0; i < node.style.length; i++) {
3755 // Approximate check
3756 if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
3757 return true;
3758 }
3759 }
3761 return false;
3762 }
3764 // "A simple indentation element is an indentation element that has no
3765 // attributes except possibly
3766 //
3767 // * "a style attribute that sets no properties other than "margin",
3768 // "border", "padding", or subproperties of those; and/or
3769 // * "a dir attribute."
3770 function isSimpleIndentationElement(node) {
3771 if (!isIndentationElement(node)) {
3772 return false;
3773 }
3775 for (var i = 0; i < node.attributes.length; i++) {
3776 if (!isHtmlNamespace(node.attributes[i].namespaceURI)
3777 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
3778 return false;
3779 }
3780 }
3782 for (var i = 0; i < node.style.length; i++) {
3783 // This is approximate, but it works well enough for my purposes.
3784 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
3785 return false;
3786 }
3787 }
3789 return true;
3790 }
3792 // "A non-list single-line container is an HTML element with local name
3793 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
3794 // or "xmp"."
3795 function isNonListSingleLineContainer(node) {
3796 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
3797 "h6", "listing", "p", "pre", "xmp"]);
3798 }
3800 // "A single-line container is either a non-list single-line container, or an
3801 // HTML element with local name "li", "dt", or "dd"."
3802 function isSingleLineContainer(node) {
3803 return isNonListSingleLineContainer(node)
3804 || isHtmlElement(node, ["li", "dt", "dd"]);
3805 }
3807 function getBlockNodeOf(node) {
3808 // "While node is an inline node, set node to its parent."
3809 while (isInlineNode(node)) {
3810 node = node.parentNode;
3811 }
3813 // "Return node."
3814 return node;
3815 }
3817 //@}
3818 ///// Assorted block formatting command algorithms /////
3819 //@{
3821 function fixDisallowedAncestors(node) {
3822 // "If node is not editable, abort these steps."
3823 if (!isEditable(node)) {
3824 return;
3825 }
3827 // "If node is not an allowed child of any of its ancestors in the same
3828 // editing host:"
3829 if (getAncestors(node).every(function(ancestor) {
3830 return !inSameEditingHost(node, ancestor)
3831 || !isAllowedChild(node, ancestor)
3832 })) {
3833 // "If node is a dd or dt, wrap the one-node list consisting of node,
3834 // with sibling criteria returning true for any dl with no attributes
3835 // and false otherwise, and new parent instructions returning the
3836 // result of calling createElement("dl") on the context object. Then
3837 // abort these steps."
3838 if (isHtmlElement(node, ["dd", "dt"])) {
3839 wrap([node],
3840 function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length },
3841 function() { return document.createElement("dl") });
3842 return;
3843 }
3845 // "If "p" is not an allowed child of the editing host of node, abort
3846 // these steps."
3847 if (!isAllowedChild("p", getEditingHostOf(node))) {
3848 return;
3849 }
3851 // "If node is not a prohibited paragraph child, abort these steps."
3852 if (!isProhibitedParagraphChild(node)) {
3853 return;
3854 }
3856 // "Set the tag name of node to the default single-line container name,
3857 // and let node be the result."
3858 node = setTagName(node, defaultSingleLineContainerName);
3860 // "Fix disallowed ancestors of node."
3861 fixDisallowedAncestors(node);
3863 // "Let children be node's children."
3864 var children = [].slice.call(node.childNodes);
3866 // "For each child in children, if child is a prohibited paragraph
3867 // child:"
3868 children.filter(isProhibitedParagraphChild)
3869 .forEach(function(child) {
3870 // "Record the values of the one-node list consisting of child, and
3871 // let values be the result."
3872 var values = recordValues([child]);
3874 // "Split the parent of the one-node list consisting of child."
3875 splitParent([child]);
3877 // "Restore the values from values."
3878 restoreValues(values);
3879 });
3881 // "Abort these steps."
3882 return;
3883 }
3885 // "Record the values of the one-node list consisting of node, and let
3886 // values be the result."
3887 var values = recordValues([node]);
3889 // "While node is not an allowed child of its parent, split the parent of
3890 // the one-node list consisting of node."
3891 while (!isAllowedChild(node, node.parentNode)) {
3892 splitParent([node]);
3893 }
3895 // "Restore the values from values."
3896 restoreValues(values);
3897 }
3899 function normalizeSublists(item) {
3900 // "If item is not an li or it is not editable or its parent is not
3901 // editable, abort these steps."
3902 if (!isHtmlElement(item, "LI")
3903 || !isEditable(item)
3904 || !isEditable(item.parentNode)) {
3905 return;
3906 }
3908 // "Let new item be null."
3909 var newItem = null;
3911 // "While item has an ol or ul child:"
3912 while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) {
3913 // "Let child be the last child of item."
3914 var child = item.lastChild;
3916 // "If child is an ol or ul, or new item is null and child is a Text
3917 // node whose data consists of zero of more space characters:"
3918 if (isHtmlElement(child, ["OL", "UL"])
3919 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
3920 // "Set new item to null."
3921 newItem = null;
3923 // "Insert child into the parent of item immediately following
3924 // item, preserving ranges."
3925 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item));
3927 // "Otherwise:"
3928 } else {
3929 // "If new item is null, let new item be the result of calling
3930 // createElement("li") on the ownerDocument of item, then insert
3931 // new item into the parent of item immediately after item."
3932 if (!newItem) {
3933 newItem = item.ownerDocument.createElement("li");
3934 item.parentNode.insertBefore(newItem, item.nextSibling);
3935 }
3937 // "Insert child into new item as its first child, preserving
3938 // ranges."
3939 movePreservingRanges(child, newItem, 0);
3940 }
3941 }
3942 }
3944 function getSelectionListState() {
3945 // "If the active range is null, return "none"."
3946 if (!getActiveRange()) {
3947 return "none";
3948 }
3950 // "Block-extend the active range, and let new range be the result."
3951 var newRange = blockExtend(getActiveRange());
3953 // "Let node list be a list of nodes, initially empty."
3954 //
3955 // "For each node contained in new range, append node to node list if the
3956 // last member of node list (if any) is not an ancestor of node; node is
3957 // editable; node is not an indentation element; and node is either an ol
3958 // or ul, or the child of an ol or ul, or an allowed child of "li"."
3959 var nodeList = getContainedNodes(newRange, function(node) {
3960 return isEditable(node)
3961 && !isIndentationElement(node)
3962 && (isHtmlElement(node, ["ol", "ul"])
3963 || isHtmlElement(node.parentNode, ["ol", "ul"])
3964 || isAllowedChild(node, "li"));
3965 });
3967 // "If node list is empty, return "none"."
3968 if (!nodeList.length) {
3969 return "none";
3970 }
3972 // "If every member of node list is either an ol or the child of an ol or
3973 // the child of an li child of an ol, and none is a ul or an ancestor of a
3974 // ul, return "ol"."
3975 if (nodeList.every(function(node) {
3976 return isHtmlElement(node, "ol")
3977 || isHtmlElement(node.parentNode, "ol")
3978 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
3979 })
3980 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) {
3981 return "ol";
3982 }
3984 // "If every member of node list is either a ul or the child of a ul or the
3985 // child of an li child of a ul, and none is an ol or an ancestor of an ol,
3986 // return "ul"."
3987 if (nodeList.every(function(node) {
3988 return isHtmlElement(node, "ul")
3989 || isHtmlElement(node.parentNode, "ul")
3990 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
3991 })
3992 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) {
3993 return "ul";
3994 }
3996 var hasOl = nodeList.some(function(node) {
3997 return isHtmlElement(node, "ol")
3998 || isHtmlElement(node.parentNode, "ol")
3999 || ("querySelector" in node && node.querySelector("ol"))
4000 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
4001 });
4002 var hasUl = nodeList.some(function(node) {
4003 return isHtmlElement(node, "ul")
4004 || isHtmlElement(node.parentNode, "ul")
4005 || ("querySelector" in node && node.querySelector("ul"))
4006 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
4007 });
4008 // "If some member of node list is either an ol or the child or ancestor of
4009 // an ol or the child of an li child of an ol, and some member of node list
4010 // is either a ul or the child or ancestor of a ul or the child of an li
4011 // child of a ul, return "mixed"."
4012 if (hasOl && hasUl) {
4013 return "mixed";
4014 }
4016 // "If some member of node list is either an ol or the child or ancestor of
4017 // an ol or the child of an li child of an ol, return "mixed ol"."
4018 if (hasOl) {
4019 return "mixed ol";
4020 }
4022 // "If some member of node list is either a ul or the child or ancestor of
4023 // a ul or the child of an li child of a ul, return "mixed ul"."
4024 if (hasUl) {
4025 return "mixed ul";
4026 }
4028 // "Return "none"."
4029 return "none";
4030 }
4032 function getAlignmentValue(node) {
4033 // "While node is neither null nor an Element, or it is an Element but its
4034 // "display" property has resolved value "inline" or "none", set node to
4035 // its parent."
4036 while ((node && node.nodeType != Node.ELEMENT_NODE)
4037 || (node.nodeType == Node.ELEMENT_NODE
4038 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
4039 node = node.parentNode;
4040 }
4042 // "If node is not an Element, return "left"."
4043 if (!node || node.nodeType != Node.ELEMENT_NODE) {
4044 return "left";
4045 }
4047 var resolvedValue = getComputedStyle(node).textAlign
4048 // Hack around browser non-standardness
4049 .replace(/^-(moz|webkit)-/, "")
4050 .replace(/^auto$/, "start");
4052 // "If node's "text-align" property has resolved value "start", return
4053 // "left" if the directionality of node is "ltr", "right" if it is "rtl"."
4054 if (resolvedValue == "start") {
4055 return getDirectionality(node) == "ltr" ? "left" : "right";
4056 }
4058 // "If node's "text-align" property has resolved value "end", return
4059 // "right" if the directionality of node is "ltr", "left" if it is "rtl"."
4060 if (resolvedValue == "end") {
4061 return getDirectionality(node) == "ltr" ? "right" : "left";
4062 }
4064 // "If node's "text-align" property has resolved value "center", "justify",
4065 // "left", or "right", return that value."
4066 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
4067 return resolvedValue;
4068 }
4070 // "Return "left"."
4071 return "left";
4072 }
4074 function getNextEquivalentPoint(node, offset) {
4075 // "If node's length is zero, return null."
4076 if (getNodeLength(node) == 0) {
4077 return null;
4078 }
4080 // "If offset is node's length, and node's parent is not null, and node is
4081 // an inline node, return (node's parent, 1 + node's index)."
4082 if (offset == getNodeLength(node)
4083 && node.parentNode
4084 && isInlineNode(node)) {
4085 return [node.parentNode, 1 + getNodeIndex(node)];
4086 }
4088 // "If node has a child with index offset, and that child's length is not
4089 // zero, and that child is an inline node, return (that child, 0)."
4090 if (0 <= offset
4091 && offset < node.childNodes.length
4092 && getNodeLength(node.childNodes[offset]) != 0
4093 && isInlineNode(node.childNodes[offset])) {
4094 return [node.childNodes[offset], 0];
4095 }
4097 // "Return null."
4098 return null;
4099 }
4101 function getPreviousEquivalentPoint(node, offset) {
4102 // "If node's length is zero, return null."
4103 if (getNodeLength(node) == 0) {
4104 return null;
4105 }
4107 // "If offset is 0, and node's parent is not null, and node is an inline
4108 // node, return (node's parent, node's index)."
4109 if (offset == 0
4110 && node.parentNode
4111 && isInlineNode(node)) {
4112 return [node.parentNode, getNodeIndex(node)];
4113 }
4115 // "If node has a child with index offset − 1, and that child's length is
4116 // not zero, and that child is an inline node, return (that child, that
4117 // child's length)."
4118 if (0 <= offset - 1
4119 && offset - 1 < node.childNodes.length
4120 && getNodeLength(node.childNodes[offset - 1]) != 0
4121 && isInlineNode(node.childNodes[offset - 1])) {
4122 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
4123 }
4125 // "Return null."
4126 return null;
4127 }
4129 function getFirstEquivalentPoint(node, offset) {
4130 // "While (node, offset)'s previous equivalent point is not null, set
4131 // (node, offset) to its previous equivalent point."
4132 var prev;
4133 while (prev = getPreviousEquivalentPoint(node, offset)) {
4134 node = prev[0];
4135 offset = prev[1];
4136 }
4138 // "Return (node, offset)."
4139 return [node, offset];
4140 }
4142 function getLastEquivalentPoint(node, offset) {
4143 // "While (node, offset)'s next equivalent point is not null, set (node,
4144 // offset) to its next equivalent point."
4145 var next;
4146 while (next = getNextEquivalentPoint(node, offset)) {
4147 node = next[0];
4148 offset = next[1];
4149 }
4151 // "Return (node, offset)."
4152 return [node, offset];
4153 }
4155 //@}
4156 ///// Block-extending a range /////
4157 //@{
4159 // "A boundary point (node, offset) is a block start point if either node's
4160 // parent is null and offset is zero; or node has a child with index offset −
4161 // 1, and that child is either a visible block node or a visible br."
4162 function isBlockStartPoint(node, offset) {
4163 return (!node.parentNode && offset == 0)
4164 || (0 <= offset - 1
4165 && offset - 1 < node.childNodes.length
4166 && isVisible(node.childNodes[offset - 1])
4167 && (isBlockNode(node.childNodes[offset - 1])
4168 || isHtmlElement(node.childNodes[offset - 1], "br")));
4169 }
4171 // "A boundary point (node, offset) is a block end point if either node's
4172 // parent is null and offset is node's length; or node has a child with index
4173 // offset, and that child is a visible block node."
4174 function isBlockEndPoint(node, offset) {
4175 return (!node.parentNode && offset == getNodeLength(node))
4176 || (offset < node.childNodes.length
4177 && isVisible(node.childNodes[offset])
4178 && isBlockNode(node.childNodes[offset]));
4179 }
4181 // "A boundary point is a block boundary point if it is either a block start
4182 // point or a block end point."
4183 function isBlockBoundaryPoint(node, offset) {
4184 return isBlockStartPoint(node, offset)
4185 || isBlockEndPoint(node, offset);
4186 }
4188 function blockExtend(range) {
4189 // "Let start node, start offset, end node, and end offset be the start
4190 // and end nodes and offsets of the range."
4191 var startNode = range.startContainer;
4192 var startOffset = range.startOffset;
4193 var endNode = range.endContainer;
4194 var endOffset = range.endOffset;
4196 // "If some ancestor container of start node is an li, set start offset to
4197 // the index of the last such li in tree order, and set start node to that
4198 // li's parent."
4199 var liAncestors = getAncestors(startNode).concat(startNode)
4200 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4201 .slice(-1);
4202 if (liAncestors.length) {
4203 startOffset = getNodeIndex(liAncestors[0]);
4204 startNode = liAncestors[0].parentNode;
4205 }
4207 // "If (start node, start offset) is not a block start point, repeat the
4208 // following steps:"
4209 if (!isBlockStartPoint(startNode, startOffset)) do {
4210 // "If start offset is zero, set it to start node's index, then set
4211 // start node to its parent."
4212 if (startOffset == 0) {
4213 startOffset = getNodeIndex(startNode);
4214 startNode = startNode.parentNode;
4216 // "Otherwise, subtract one from start offset."
4217 } else {
4218 startOffset--;
4219 }
4221 // "If (start node, start offset) is a block boundary point, break from
4222 // this loop."
4223 } while (!isBlockBoundaryPoint(startNode, startOffset));
4225 // "While start offset is zero and start node's parent is not null, set
4226 // start offset to start node's index, then set start node to its parent."
4227 while (startOffset == 0
4228 && startNode.parentNode) {
4229 startOffset = getNodeIndex(startNode);
4230 startNode = startNode.parentNode;
4231 }
4233 // "If some ancestor container of end node is an li, set end offset to one
4234 // plus the index of the last such li in tree order, and set end node to
4235 // that li's parent."
4236 var liAncestors = getAncestors(endNode).concat(endNode)
4237 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4238 .slice(-1);
4239 if (liAncestors.length) {
4240 endOffset = 1 + getNodeIndex(liAncestors[0]);
4241 endNode = liAncestors[0].parentNode;
4242 }
4244 // "If (end node, end offset) is not a block end point, repeat the
4245 // following steps:"
4246 if (!isBlockEndPoint(endNode, endOffset)) do {
4247 // "If end offset is end node's length, set it to one plus end node's
4248 // index, then set end node to its parent."
4249 if (endOffset == getNodeLength(endNode)) {
4250 endOffset = 1 + getNodeIndex(endNode);
4251 endNode = endNode.parentNode;
4253 // "Otherwise, add one to end offset.
4254 } else {
4255 endOffset++;
4256 }
4258 // "If (end node, end offset) is a block boundary point, break from
4259 // this loop."
4260 } while (!isBlockBoundaryPoint(endNode, endOffset));
4262 // "While end offset is end node's length and end node's parent is not
4263 // null, set end offset to one plus end node's index, then set end node to
4264 // its parent."
4265 while (endOffset == getNodeLength(endNode)
4266 && endNode.parentNode) {
4267 endOffset = 1 + getNodeIndex(endNode);
4268 endNode = endNode.parentNode;
4269 }
4271 // "Let new range be a new range whose start and end nodes and offsets
4272 // are start node, start offset, end node, and end offset."
4273 var newRange = startNode.ownerDocument.createRange();
4274 newRange.setStart(startNode, startOffset);
4275 newRange.setEnd(endNode, endOffset);
4277 // "Return new range."
4278 return newRange;
4279 }
4281 function followsLineBreak(node) {
4282 // "Let offset be zero."
4283 var offset = 0;
4285 // "While (node, offset) is not a block boundary point:"
4286 while (!isBlockBoundaryPoint(node, offset)) {
4287 // "If node has a visible child with index offset minus one, return
4288 // false."
4289 if (0 <= offset - 1
4290 && offset - 1 < node.childNodes.length
4291 && isVisible(node.childNodes[offset - 1])) {
4292 return false;
4293 }
4295 // "If offset is zero or node has no children, set offset to node's
4296 // index, then set node to its parent."
4297 if (offset == 0
4298 || !node.hasChildNodes()) {
4299 offset = getNodeIndex(node);
4300 node = node.parentNode;
4302 // "Otherwise, set node to its child with index offset minus one, then
4303 // set offset to node's length."
4304 } else {
4305 node = node.childNodes[offset - 1];
4306 offset = getNodeLength(node);
4307 }
4308 }
4310 // "Return true."
4311 return true;
4312 }
4314 function precedesLineBreak(node) {
4315 // "Let offset be node's length."
4316 var offset = getNodeLength(node);
4318 // "While (node, offset) is not a block boundary point:"
4319 while (!isBlockBoundaryPoint(node, offset)) {
4320 // "If node has a visible child with index offset, return false."
4321 if (offset < node.childNodes.length
4322 && isVisible(node.childNodes[offset])) {
4323 return false;
4324 }
4326 // "If offset is node's length or node has no children, set offset to
4327 // one plus node's index, then set node to its parent."
4328 if (offset == getNodeLength(node)
4329 || !node.hasChildNodes()) {
4330 offset = 1 + getNodeIndex(node);
4331 node = node.parentNode;
4333 // "Otherwise, set node to its child with index offset and set offset
4334 // to zero."
4335 } else {
4336 node = node.childNodes[offset];
4337 offset = 0;
4338 }
4339 }
4341 // "Return true."
4342 return true;
4343 }
4345 //@}
4346 ///// Recording and restoring overrides /////
4347 //@{
4349 function recordCurrentOverrides() {
4350 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4351 // initially empty."
4352 var overrides = [];
4354 // "If there is a value override for "createLink", add ("createLink", value
4355 // override for "createLink") to overrides."
4356 if (getValueOverride("createlink") !== undefined) {
4357 overrides.push(["createlink", getValueOverride("createlink")]);
4358 }
4360 // "For each command in the list "bold", "italic", "strikethrough",
4361 // "subscript", "superscript", "underline", in order: if there is a state
4362 // override for command, add (command, command's state override) to
4363 // overrides."
4364 ["bold", "italic", "strikethrough", "subscript", "superscript",
4365 "underline"].forEach(function(command) {
4366 if (getStateOverride(command) !== undefined) {
4367 overrides.push([command, getStateOverride(command)]);
4368 }
4369 });
4371 // "For each command in the list "fontName", "fontSize", "foreColor",
4372 // "hiliteColor", in order: if there is a value override for command, add
4373 // (command, command's value override) to overrides."
4374 ["fontname", "fontsize", "forecolor",
4375 "hilitecolor"].forEach(function(command) {
4376 if (getValueOverride(command) !== undefined) {
4377 overrides.push([command, getValueOverride(command)]);
4378 }
4379 });
4381 // "Return overrides."
4382 return overrides;
4383 }
4385 function recordCurrentStatesAndValues() {
4386 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4387 // initially empty."
4388 var overrides = [];
4390 // "Let node be the first formattable node effectively contained in the
4391 // active range, or null if there is none."
4392 var node = getAllEffectivelyContainedNodes(getActiveRange())
4393 .filter(isFormattableNode)[0];
4395 // "If node is null, return overrides."
4396 if (!node) {
4397 return overrides;
4398 }
4400 // "Add ("createLink", node's effective command value for "createLink") to
4401 // overrides."
4402 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]);
4404 // "For each command in the list "bold", "italic", "strikethrough",
4405 // "subscript", "superscript", "underline", in order: if node's effective
4406 // command value for command is one of its inline command activated values,
4407 // add (command, true) to overrides, and otherwise add (command, false) to
4408 // overrides."
4409 ["bold", "italic", "strikethrough", "subscript", "superscript",
4410 "underline"].forEach(function(command) {
4411 if (commands[command].inlineCommandActivatedValues
4412 .indexOf(getEffectiveCommandValue(node, command)) != -1) {
4413 overrides.push([command, true]);
4414 } else {
4415 overrides.push([command, false]);
4416 }
4417 });
4419 // "For each command in the list "fontName", "foreColor", "hiliteColor", in
4420 // order: add (command, command's value) to overrides."
4421 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
4422 overrides.push([command, commands[command].value()]);
4423 });
4425 // "Add ("fontSize", node's effective command value for "fontSize") to
4426 // overrides."
4427 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
4429 // "Return overrides."
4430 return overrides;
4431 }
4433 function restoreStatesAndValues(overrides) {
4434 // "Let node be the first formattable node effectively contained in the
4435 // active range, or null if there is none."
4436 var node = getAllEffectivelyContainedNodes(getActiveRange())
4437 .filter(isFormattableNode)[0];
4439 // "If node is not null, then for each (command, override) pair in
4440 // overrides, in order:"
4441 if (node) {
4442 for (var i = 0; i < overrides.length; i++) {
4443 var command = overrides[i][0];
4444 var override = overrides[i][1];
4446 // "If override is a boolean, and queryCommandState(command)
4447 // returns something different from override, take the action for
4448 // command, with value equal to the empty string."
4449 if (typeof override == "boolean"
4450 && myQueryCommandState(command) != override) {
4451 commands[command].action("");
4453 // "Otherwise, if override is a string, and command is neither
4454 // "createLink" nor "fontSize", and queryCommandValue(command)
4455 // returns something not equivalent to override, take the action
4456 // for command, with value equal to override."
4457 } else if (typeof override == "string"
4458 && command != "createlink"
4459 && command != "fontsize"
4460 && !areEquivalentValues(command, myQueryCommandValue(command), override)) {
4461 commands[command].action(override);
4463 // "Otherwise, if override is a string; and command is
4464 // "createLink"; and either there is a value override for
4465 // "createLink" that is not equal to override, or there is no value
4466 // override for "createLink" and node's effective command value for
4467 // "createLink" is not equal to override: take the action for
4468 // "createLink", with value equal to override."
4469 } else if (typeof override == "string"
4470 && command == "createlink"
4471 && (
4472 (
4473 getValueOverride("createlink") !== undefined
4474 && getValueOverride("createlink") !== override
4475 ) || (
4476 getValueOverride("createlink") === undefined
4477 && getEffectiveCommandValue(node, "createlink") !== override
4478 )
4479 )) {
4480 commands.createlink.action(override);
4482 // "Otherwise, if override is a string; and command is "fontSize";
4483 // and either there is a value override for "fontSize" that is not
4484 // equal to override, or there is no value override for "fontSize"
4485 // and node's effective command value for "fontSize" is not loosely
4486 // equivalent to override:"
4487 } else if (typeof override == "string"
4488 && command == "fontsize"
4489 && (
4490 (
4491 getValueOverride("fontsize") !== undefined
4492 && getValueOverride("fontsize") !== override
4493 ) || (
4494 getValueOverride("fontsize") === undefined
4495 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
4496 )
4497 )) {
4498 // "Convert override to an integer number of pixels, and set
4499 // override to the legacy font size for the result."
4500 override = getLegacyFontSize(override);
4502 // "Take the action for "fontSize", with value equal to
4503 // override."
4504 commands.fontsize.action(override);
4506 // "Otherwise, continue this loop from the beginning."
4507 } else {
4508 continue;
4509 }
4511 // "Set node to the first formattable node effectively contained in
4512 // the active range, if there is one."
4513 node = getAllEffectivelyContainedNodes(getActiveRange())
4514 .filter(isFormattableNode)[0]
4515 || node;
4516 }
4518 // "Otherwise, for each (command, override) pair in overrides, in order:"
4519 } else {
4520 for (var i = 0; i < overrides.length; i++) {
4521 var command = overrides[i][0];
4522 var override = overrides[i][1];
4524 // "If override is a boolean, set the state override for command to
4525 // override."
4526 if (typeof override == "boolean") {
4527 setStateOverride(command, override);
4528 }
4530 // "If override is a string, set the value override for command to
4531 // override."
4532 if (typeof override == "string") {
4533 setValueOverride(command, override);
4534 }
4535 }
4536 }
4537 }
4539 //@}
4540 ///// Deleting the selection /////
4541 //@{
4543 // The flags argument is a dictionary that can have blockMerging,
4544 // stripWrappers, and/or direction as keys.
4545 function deleteSelection(flags) {
4546 if (flags === undefined) {
4547 flags = {};
4548 }
4550 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true;
4551 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
4552 var direction = "direction" in flags ? flags.direction : "forward";
4554 // "If the active range is null, abort these steps and do nothing."
4555 if (!getActiveRange()) {
4556 return;
4557 }
4559 // "Canonicalize whitespace at the active range's start."
4560 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
4562 // "Canonicalize whitespace at the active range's end."
4563 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset);
4565 // "Let (start node, start offset) be the last equivalent point for the
4566 // active range's start."
4567 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset);
4568 var startNode = start[0];
4569 var startOffset = start[1];
4571 // "Let (end node, end offset) be the first equivalent point for the active
4572 // range's end."
4573 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset);
4574 var endNode = end[0];
4575 var endOffset = end[1];
4577 // "If (end node, end offset) is not after (start node, start offset):"
4578 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
4579 // "If direction is "forward", call collapseToStart() on the context
4580 // object's Selection."
4581 //
4582 // Here and in a few other places, we check rangeCount to work around a
4583 // WebKit bug: it will sometimes incorrectly remove ranges from the
4584 // selection if nodes are removed, so collapseToStart() will throw.
4585 // This will break everything if we're using an actual selection, but
4586 // if getActiveRange() is really just returning globalRange and that's
4587 // all we care about, it will work fine. I only add the extra check
4588 // for errors I actually hit in testing.
4589 if (direction == "forward") {
4590 if (getSelection().rangeCount) {
4591 getSelection().collapseToStart();
4592 }
4593 getActiveRange().collapse(true);
4595 // "Otherwise, call collapseToEnd() on the context object's Selection."
4596 } else {
4597 getSelection().collapseToEnd();
4598 getActiveRange().collapse(false);
4599 }
4601 // "Abort these steps."
4602 return;
4603 }
4605 // "If start node is a Text node and start offset is 0, set start offset to
4606 // the index of start node, then set start node to its parent."
4607 if (startNode.nodeType == Node.TEXT_NODE
4608 && startOffset == 0) {
4609 startOffset = getNodeIndex(startNode);
4610 startNode = startNode.parentNode;
4611 }
4613 // "If end node is a Text node and end offset is its length, set end offset
4614 // to one plus the index of end node, then set end node to its parent."
4615 if (endNode.nodeType == Node.TEXT_NODE
4616 && endOffset == getNodeLength(endNode)) {
4617 endOffset = 1 + getNodeIndex(endNode);
4618 endNode = endNode.parentNode;
4619 }
4621 // "Call collapse(start node, start offset) on the context object's
4622 // Selection."
4623 getSelection().collapse(startNode, startOffset);
4624 getActiveRange().setStart(startNode, startOffset);
4626 // "Call extend(end node, end offset) on the context object's Selection."
4627 getSelection().extend(endNode, endOffset);
4628 getActiveRange().setEnd(endNode, endOffset);
4630 // "Let start block be the active range's start node."
4631 var startBlock = getActiveRange().startContainer;
4633 // "While start block's parent is in the same editing host and start block
4634 // is an inline node, set start block to its parent."
4635 while (inSameEditingHost(startBlock, startBlock.parentNode)
4636 && isInlineNode(startBlock)) {
4637 startBlock = startBlock.parentNode;
4638 }
4640 // "If start block is neither a block node nor an editing host, or "span"
4641 // is not an allowed child of start block, or start block is a td or th,
4642 // set start block to null."
4643 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
4644 || !isAllowedChild("span", startBlock)
4645 || isHtmlElement(startBlock, ["td", "th"])) {
4646 startBlock = null;
4647 }
4649 // "Let end block be the active range's end node."
4650 var endBlock = getActiveRange().endContainer;
4652 // "While end block's parent is in the same editing host and end block is
4653 // an inline node, set end block to its parent."
4654 while (inSameEditingHost(endBlock, endBlock.parentNode)
4655 && isInlineNode(endBlock)) {
4656 endBlock = endBlock.parentNode;
4657 }
4659 // "If end block is neither a block node nor an editing host, or "span" is
4660 // not an allowed child of end block, or end block is a td or th, set end
4661 // block to null."
4662 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
4663 || !isAllowedChild("span", endBlock)
4664 || isHtmlElement(endBlock, ["td", "th"])) {
4665 endBlock = null;
4666 }
4668 // "Record current states and values, and let overrides be the result."
4669 var overrides = recordCurrentStatesAndValues();
4671 // "If start node and end node are the same, and start node is an editable
4672 // Text node:"
4673 if (startNode == endNode
4674 && isEditable(startNode)
4675 && startNode.nodeType == Node.TEXT_NODE) {
4676 // "Call deleteData(start offset, end offset − start offset) on start
4677 // node."
4678 startNode.deleteData(startOffset, endOffset - startOffset);
4680 // "Canonicalize whitespace at (start node, start offset), with fix
4681 // collapsed space false."
4682 canonicalizeWhitespace(startNode, startOffset, false);
4684 // "If direction is "forward", call collapseToStart() on the context
4685 // object's Selection."
4686 if (direction == "forward") {
4687 if (getSelection().rangeCount) {
4688 getSelection().collapseToStart();
4689 }
4690 getActiveRange().collapse(true);
4692 // "Otherwise, call collapseToEnd() on the context object's Selection."
4693 } else {
4694 getSelection().collapseToEnd();
4695 getActiveRange().collapse(false);
4696 }
4698 // "Restore states and values from overrides."
4699 restoreStatesAndValues(overrides);
4701 // "Abort these steps."
4702 return;
4703 }
4705 // "If start node is an editable Text node, call deleteData() on it, with
4706 // start offset as the first argument and (length of start node − start
4707 // offset) as the second argument."
4708 if (isEditable(startNode)
4709 && startNode.nodeType == Node.TEXT_NODE) {
4710 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
4711 }
4713 // "Let node list be a list of nodes, initially empty."
4714 //
4715 // "For each node contained in the active range, append node to node list
4716 // if the last member of node list (if any) is not an ancestor of node;
4717 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
4718 var nodeList = getContainedNodes(getActiveRange(),
4719 function(node) {
4720 return isEditable(node)
4721 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
4722 }
4723 );
4725 // "For each node in node list:"
4726 for (var i = 0; i < nodeList.length; i++) {
4727 var node = nodeList[i];
4729 // "Let parent be the parent of node."
4730 var parent_ = node.parentNode;
4732 // "Remove node from parent."
4733 parent_.removeChild(node);
4735 // "If the block node of parent has no visible children, and parent is
4736 // editable or an editing host, call createElement("br") on the context
4737 // object and append the result as the last child of parent."
4738 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
4739 && (isEditable(parent_) || isEditingHost(parent_))) {
4740 parent_.appendChild(document.createElement("br"));
4741 }
4743 // "If strip wrappers is true or parent is not an ancestor container of
4744 // start node, while parent is an editable inline node with length 0,
4745 // let grandparent be the parent of parent, then remove parent from
4746 // grandparent, then set parent to grandparent."
4747 if (stripWrappers
4748 || (!isAncestor(parent_, startNode) && parent_ != startNode)) {
4749 while (isEditable(parent_)
4750 && isInlineNode(parent_)
4751 && getNodeLength(parent_) == 0) {
4752 var grandparent = parent_.parentNode;
4753 grandparent.removeChild(parent_);
4754 parent_ = grandparent;
4755 }
4756 }
4757 }
4759 // "If end node is an editable Text node, call deleteData(0, end offset) on
4760 // it."
4761 if (isEditable(endNode)
4762 && endNode.nodeType == Node.TEXT_NODE) {
4763 endNode.deleteData(0, endOffset);
4764 }
4766 // "Canonicalize whitespace at the active range's start, with fix collapsed
4767 // space false."
4768 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
4770 // "Canonicalize whitespace at the active range's end, with fix collapsed
4771 // space false."
4772 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
4774 // "If block merging is false, or start block or end block is null, or
4775 // start block is not in the same editing host as end block, or start block
4776 // and end block are the same:"
4777 if (!blockMerging
4778 || !startBlock
4779 || !endBlock
4780 || !inSameEditingHost(startBlock, endBlock)
4781 || startBlock == endBlock) {
4782 // "If direction is "forward", call collapseToStart() on the context
4783 // object's Selection."
4784 if (direction == "forward") {
4785 if (getSelection().rangeCount) {
4786 getSelection().collapseToStart();
4787 }
4788 getActiveRange().collapse(true);
4790 // "Otherwise, call collapseToEnd() on the context object's Selection."
4791 } else {
4792 if (getSelection().rangeCount) {
4793 getSelection().collapseToEnd();
4794 }
4795 getActiveRange().collapse(false);
4796 }
4798 // "Restore states and values from overrides."
4799 restoreStatesAndValues(overrides);
4801 // "Abort these steps."
4802 return;
4803 }
4805 // "If start block has one child, which is a collapsed block prop, remove
4806 // its child from it."
4807 if (startBlock.children.length == 1
4808 && isCollapsedBlockProp(startBlock.firstChild)) {
4809 startBlock.removeChild(startBlock.firstChild);
4810 }
4812 // "If start block is an ancestor of end block:"
4813 if (isAncestor(startBlock, endBlock)) {
4814 // "Let reference node be end block."
4815 var referenceNode = endBlock;
4817 // "While reference node is not a child of start block, set reference
4818 // node to its parent."
4819 while (referenceNode.parentNode != startBlock) {
4820 referenceNode = referenceNode.parentNode;
4821 }
4823 // "Call collapse() on the context object's Selection, with first
4824 // argument start block and second argument the index of reference
4825 // node."
4826 getSelection().collapse(startBlock, getNodeIndex(referenceNode));
4827 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
4828 getActiveRange().collapse(true);
4830 // "If end block has no children:"
4831 if (!endBlock.hasChildNodes()) {
4832 // "While end block is editable and is the only child of its parent
4833 // and is not a child of start block, let parent equal end block,
4834 // then remove end block from parent, then set end block to
4835 // parent."
4836 while (isEditable(endBlock)
4837 && endBlock.parentNode.childNodes.length == 1
4838 && endBlock.parentNode != startBlock) {
4839 var parent_ = endBlock;
4840 parent_.removeChild(endBlock);
4841 endBlock = parent_;
4842 }
4844 // "If end block is editable and is not an inline node, and its
4845 // previousSibling and nextSibling are both inline nodes, call
4846 // createElement("br") on the context object and insert it into end
4847 // block's parent immediately after end block."
4848 if (isEditable(endBlock)
4849 && !isInlineNode(endBlock)
4850 && isInlineNode(endBlock.previousSibling)
4851 && isInlineNode(endBlock.nextSibling)) {
4852 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
4853 }
4855 // "If end block is editable, remove it from its parent."
4856 if (isEditable(endBlock)) {
4857 endBlock.parentNode.removeChild(endBlock);
4858 }
4860 // "Restore states and values from overrides."
4861 restoreStatesAndValues(overrides);
4863 // "Abort these steps."
4864 return;
4865 }
4867 // "If end block's firstChild is not an inline node, restore states and
4868 // values from overrides, then abort these steps."
4869 if (!isInlineNode(endBlock.firstChild)) {
4870 restoreStatesAndValues(overrides);
4871 return;
4872 }
4874 // "Let children be a list of nodes, initially empty."
4875 var children = [];
4877 // "Append the first child of end block to children."
4878 children.push(endBlock.firstChild);
4880 // "While children's last member is not a br, and children's last
4881 // member's nextSibling is an inline node, append children's last
4882 // member's nextSibling to children."
4883 while (!isHtmlElement(children[children.length - 1], "br")
4884 && isInlineNode(children[children.length - 1].nextSibling)) {
4885 children.push(children[children.length - 1].nextSibling);
4886 }
4888 // "Record the values of children, and let values be the result."
4889 var values = recordValues(children);
4891 // "While children's first member's parent is not start block, split
4892 // the parent of children."
4893 while (children[0].parentNode != startBlock) {
4894 splitParent(children);
4895 }
4897 // "If children's first member's previousSibling is an editable br,
4898 // remove that br from its parent."
4899 if (isEditable(children[0].previousSibling)
4900 && isHtmlElement(children[0].previousSibling, "br")) {
4901 children[0].parentNode.removeChild(children[0].previousSibling);
4902 }
4904 // "Otherwise, if start block is a descendant of end block:"
4905 } else if (isDescendant(startBlock, endBlock)) {
4906 // "Call collapse() on the context object's Selection, with first
4907 // argument start block and second argument start block's length."
4908 getSelection().collapse(startBlock, getNodeLength(startBlock));
4909 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4910 getActiveRange().collapse(true);
4912 // "Let reference node be start block."
4913 var referenceNode = startBlock;
4915 // "While reference node is not a child of end block, set reference
4916 // node to its parent."
4917 while (referenceNode.parentNode != endBlock) {
4918 referenceNode = referenceNode.parentNode;
4919 }
4921 // "If reference node's nextSibling is an inline node and start block's
4922 // lastChild is a br, remove start block's lastChild from it."
4923 if (isInlineNode(referenceNode.nextSibling)
4924 && isHtmlElement(startBlock.lastChild, "br")) {
4925 startBlock.removeChild(startBlock.lastChild);
4926 }
4928 // "Let nodes to move be a list of nodes, initially empty."
4929 var nodesToMove = [];
4931 // "If reference node's nextSibling is neither null nor a block node,
4932 // append it to nodes to move."
4933 if (referenceNode.nextSibling
4934 && !isBlockNode(referenceNode.nextSibling)) {
4935 nodesToMove.push(referenceNode.nextSibling);
4936 }
4938 // "While nodes to move is nonempty and its last member isn't a br and
4939 // its last member's nextSibling is neither null nor a block node,
4940 // append its last member's nextSibling to nodes to move."
4941 if (nodesToMove.length
4942 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
4943 && nodesToMove[nodesToMove.length - 1].nextSibling
4944 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
4945 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
4946 }
4948 // "Record the values of nodes to move, and let values be the result."
4949 var values = recordValues(nodesToMove);
4951 // "For each node in nodes to move, append node as the last child of
4952 // start block, preserving ranges."
4953 nodesToMove.forEach(function(node) {
4954 movePreservingRanges(node, startBlock, -1);
4955 });
4957 // "Otherwise:"
4958 } else {
4959 // "Call collapse() on the context object's Selection, with first
4960 // argument start block and second argument start block's length."
4961 getSelection().collapse(startBlock, getNodeLength(startBlock));
4962 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4963 getActiveRange().collapse(true);
4965 // "If end block's firstChild is an inline node and start block's
4966 // lastChild is a br, remove start block's lastChild from it."
4967 if (isInlineNode(endBlock.firstChild)
4968 && isHtmlElement(startBlock.lastChild, "br")) {
4969 startBlock.removeChild(startBlock.lastChild);
4970 }
4972 // "Record the values of end block's children, and let values be the
4973 // result."
4974 var values = recordValues([].slice.call(endBlock.childNodes));
4976 // "While end block has children, append the first child of end block
4977 // to start block, preserving ranges."
4978 while (endBlock.hasChildNodes()) {
4979 movePreservingRanges(endBlock.firstChild, startBlock, -1);
4980 }
4982 // "While end block has no children, let parent be the parent of end
4983 // block, then remove end block from parent, then set end block to
4984 // parent."
4985 while (!endBlock.hasChildNodes()) {
4986 var parent_ = endBlock.parentNode;
4987 parent_.removeChild(endBlock);
4988 endBlock = parent_;
4989 }
4990 }
4992 // "Let ancestor be start block."
4993 var ancestor = startBlock;
4995 // "While ancestor has an inclusive ancestor ol in the same editing host
4996 // whose nextSibling is also an ol in the same editing host, or an
4997 // inclusive ancestor ul in the same editing host whose nextSibling is also
4998 // a ul in the same editing host:"
4999 while (getInclusiveAncestors(ancestor).some(function(node) {
5000 return inSameEditingHost(ancestor, node)
5001 && (
5002 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol"))
5003 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
5004 ) && inSameEditingHost(ancestor, node.nextSibling);
5005 })) {
5006 // "While ancestor and its nextSibling are not both ols in the same
5007 // editing host, and are also not both uls in the same editing host,
5008 // set ancestor to its parent."
5009 while (!(
5010 isHtmlElement(ancestor, "ol")
5011 && isHtmlElement(ancestor.nextSibling, "ol")
5012 && inSameEditingHost(ancestor, ancestor.nextSibling)
5013 ) && !(
5014 isHtmlElement(ancestor, "ul")
5015 && isHtmlElement(ancestor.nextSibling, "ul")
5016 && inSameEditingHost(ancestor, ancestor.nextSibling)
5017 )) {
5018 ancestor = ancestor.parentNode;
5019 }
5021 // "While ancestor's nextSibling has children, append ancestor's
5022 // nextSibling's firstChild as the last child of ancestor, preserving
5023 // ranges."
5024 while (ancestor.nextSibling.hasChildNodes()) {
5025 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
5026 }
5028 // "Remove ancestor's nextSibling from its parent."
5029 ancestor.parentNode.removeChild(ancestor.nextSibling);
5030 }
5032 // "Restore the values from values."
5033 restoreValues(values);
5035 // "If start block has no children, call createElement("br") on the context
5036 // object and append the result as the last child of start block."
5037 if (!startBlock.hasChildNodes()) {
5038 startBlock.appendChild(document.createElement("br"));
5039 }
5041 // "Remove extraneous line breaks at the end of start block."
5042 removeExtraneousLineBreaksAtTheEndOf(startBlock);
5044 // "Restore states and values from overrides."
5045 restoreStatesAndValues(overrides);
5046 }
5049 //@}
5050 ///// Splitting a node list's parent /////
5051 //@{
5053 function splitParent(nodeList) {
5054 // "Let original parent be the parent of the first member of node list."
5055 var originalParent = nodeList[0].parentNode;
5057 // "If original parent is not editable or its parent is null, do nothing
5058 // and abort these steps."
5059 if (!isEditable(originalParent)
5060 || !originalParent.parentNode) {
5061 return;
5062 }
5064 // "If the first child of original parent is in node list, remove
5065 // extraneous line breaks before original parent."
5066 if (nodeList.indexOf(originalParent.firstChild) != -1) {
5067 removeExtraneousLineBreaksBefore(originalParent);
5068 }
5070 // "If the first child of original parent is in node list, and original
5071 // parent follows a line break, set follows line break to true. Otherwise,
5072 // set follows line break to false."
5073 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
5074 && followsLineBreak(originalParent);
5076 // "If the last child of original parent is in node list, and original
5077 // parent precedes a line break, set precedes line break to true.
5078 // Otherwise, set precedes line break to false."
5079 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
5080 && precedesLineBreak(originalParent);
5082 // "If the first child of original parent is not in node list, but its last
5083 // child is:"
5084 if (nodeList.indexOf(originalParent.firstChild) == -1
5085 && nodeList.indexOf(originalParent.lastChild) != -1) {
5086 // "For each node in node list, in reverse order, insert node into the
5087 // parent of original parent immediately after original parent,
5088 // preserving ranges."
5089 for (var i = nodeList.length - 1; i >= 0; i--) {
5090 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent));
5091 }
5093 // "If precedes line break is true, and the last member of node list
5094 // does not precede a line break, call createElement("br") on the
5095 // context object and insert the result immediately after the last
5096 // member of node list."
5097 if (precedesLineBreak_
5098 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5099 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5100 }
5102 // "Remove extraneous line breaks at the end of original parent."
5103 removeExtraneousLineBreaksAtTheEndOf(originalParent);
5105 // "Abort these steps."
5106 return;
5107 }
5109 // "If the first child of original parent is not in node list:"
5110 if (nodeList.indexOf(originalParent.firstChild) == -1) {
5111 // "Let cloned parent be the result of calling cloneNode(false) on
5112 // original parent."
5113 var clonedParent = originalParent.cloneNode(false);
5115 // "If original parent has an id attribute, unset it."
5116 originalParent.removeAttribute("id");
5118 // "Insert cloned parent into the parent of original parent immediately
5119 // before original parent."
5120 originalParent.parentNode.insertBefore(clonedParent, originalParent);
5122 // "While the previousSibling of the first member of node list is not
5123 // null, append the first child of original parent as the last child of
5124 // cloned parent, preserving ranges."
5125 while (nodeList[0].previousSibling) {
5126 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length);
5127 }
5128 }
5130 // "For each node in node list, insert node into the parent of original
5131 // parent immediately before original parent, preserving ranges."
5132 for (var i = 0; i < nodeList.length; i++) {
5133 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent));
5134 }
5136 // "If follows line break is true, and the first member of node list does
5137 // not follow a line break, call createElement("br") on the context object
5138 // and insert the result immediately before the first member of node list."
5139 if (followsLineBreak_
5140 && !followsLineBreak(nodeList[0])) {
5141 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
5142 }
5144 // "If the last member of node list is an inline node other than a br, and
5145 // the first child of original parent is a br, and original parent is not
5146 // an inline node, remove the first child of original parent from original
5147 // parent."
5148 if (isInlineNode(nodeList[nodeList.length - 1])
5149 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
5150 && isHtmlElement(originalParent.firstChild, "br")
5151 && !isInlineNode(originalParent)) {
5152 originalParent.removeChild(originalParent.firstChild);
5153 }
5155 // "If original parent has no children:"
5156 if (!originalParent.hasChildNodes()) {
5157 // "Remove original parent from its parent."
5158 originalParent.parentNode.removeChild(originalParent);
5160 // "If precedes line break is true, and the last member of node list
5161 // does not precede a line break, call createElement("br") on the
5162 // context object and insert the result immediately after the last
5163 // member of node list."
5164 if (precedesLineBreak_
5165 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5166 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5167 }
5169 // "Otherwise, remove extraneous line breaks before original parent."
5170 } else {
5171 removeExtraneousLineBreaksBefore(originalParent);
5172 }
5174 // "If node list's last member's nextSibling is null, but its parent is not
5175 // null, remove extraneous line breaks at the end of node list's last
5176 // member's parent."
5177 if (!nodeList[nodeList.length - 1].nextSibling
5178 && nodeList[nodeList.length - 1].parentNode) {
5179 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
5180 }
5181 }
5183 // "To remove a node node while preserving its descendants, split the parent of
5184 // node's children if it has any. If it has no children, instead remove it from
5185 // its parent."
5186 function removePreservingDescendants(node) {
5187 if (node.hasChildNodes()) {
5188 splitParent([].slice.call(node.childNodes));
5189 } else {
5190 node.parentNode.removeChild(node);
5191 }
5192 }
5195 //@}
5196 ///// Canonical space sequences /////
5197 //@{
5199 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
5200 // "If n is zero, return the empty string."
5201 if (n == 0) {
5202 return "";
5203 }
5205 // "If n is one and both non-breaking start and non-breaking end are false,
5206 // return a single space (U+0020)."
5207 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
5208 return " ";
5209 }
5211 // "If n is one, return a single non-breaking space (U+00A0)."
5212 if (n == 1) {
5213 return "\xa0";
5214 }
5216 // "Let buffer be the empty string."
5217 var buffer = "";
5219 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
5220 // Otherwise, let it be U+0020 U+00A0."
5221 var repeatedPair;
5222 if (nonBreakingStart) {
5223 repeatedPair = "\xa0 ";
5224 } else {
5225 repeatedPair = " \xa0";
5226 }
5228 // "While n is greater than three, append repeated pair to buffer and
5229 // subtract two from n."
5230 while (n > 3) {
5231 buffer += repeatedPair;
5232 n -= 2;
5233 }
5235 // "If n is three, append a three-element string to buffer depending on
5236 // non-breaking start and non-breaking end:"
5237 if (n == 3) {
5238 buffer +=
5239 !nonBreakingStart && !nonBreakingEnd ? " \xa0 "
5240 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
5241 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
5242 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
5243 : "impossible";
5245 // "Otherwise, append a two-element string to buffer depending on
5246 // non-breaking start and non-breaking end:"
5247 } else {
5248 buffer +=
5249 !nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5250 : nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5251 : !nonBreakingStart && nonBreakingEnd ? " \xa0"
5252 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
5253 : "impossible";
5254 }
5256 // "Return buffer."
5257 return buffer;
5258 }
5260 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
5261 if (fixCollapsedSpace === undefined) {
5262 // "an optional boolean argument fix collapsed space that defaults to
5263 // true"
5264 fixCollapsedSpace = true;
5265 }
5267 // "If node is neither editable nor an editing host, abort these steps."
5268 if (!isEditable(node) && !isEditingHost(node)) {
5269 return;
5270 }
5272 // "Let start node equal node and let start offset equal offset."
5273 var startNode = node;
5274 var startOffset = offset;
5276 // "Repeat the following steps:"
5277 while (true) {
5278 // "If start node has a child in the same editing host with index start
5279 // offset minus one, set start node to that child, then set start
5280 // offset to start node's length."
5281 if (0 <= startOffset - 1
5282 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
5283 startNode = startNode.childNodes[startOffset - 1];
5284 startOffset = getNodeLength(startNode);
5286 // "Otherwise, if start offset is zero and start node does not follow a
5287 // line break and start node's parent is in the same editing host, set
5288 // start offset to start node's index, then set start node to its
5289 // parent."
5290 } else if (startOffset == 0
5291 && !followsLineBreak(startNode)
5292 && inSameEditingHost(startNode, startNode.parentNode)) {
5293 startOffset = getNodeIndex(startNode);
5294 startNode = startNode.parentNode;
5296 // "Otherwise, if start node is a Text node and its parent's resolved
5297 // value for "white-space" is neither "pre" nor "pre-wrap" and start
5298 // offset is not zero and the (start offset − 1)st element of start
5299 // node's data is a space (0x0020) or non-breaking space (0x00A0),
5300 // subtract one from start offset."
5301 } else if (startNode.nodeType == Node.TEXT_NODE
5302 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
5303 && startOffset != 0
5304 && /[ \xa0]/.test(startNode.data[startOffset - 1])) {
5305 startOffset--;
5307 // "Otherwise, break from this loop."
5308 } else {
5309 break;
5310 }
5311 }
5313 // "Let end node equal start node and end offset equal start offset."
5314 var endNode = startNode;
5315 var endOffset = startOffset;
5317 // "Let length equal zero."
5318 var length = 0;
5320 // "Let collapse spaces be true if start offset is zero and start node
5321 // follows a line break, otherwise false."
5322 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
5324 // "Repeat the following steps:"
5325 while (true) {
5326 // "If end node has a child in the same editing host with index end
5327 // offset, set end node to that child, then set end offset to zero."
5328 if (endOffset < endNode.childNodes.length
5329 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
5330 endNode = endNode.childNodes[endOffset];
5331 endOffset = 0;
5333 // "Otherwise, if end offset is end node's length and end node does not
5334 // precede a line break and end node's parent is in the same editing
5335 // host, set end offset to one plus end node's index, then set end node
5336 // to its parent."
5337 } else if (endOffset == getNodeLength(endNode)
5338 && !precedesLineBreak(endNode)
5339 && inSameEditingHost(endNode, endNode.parentNode)) {
5340 endOffset = 1 + getNodeIndex(endNode);
5341 endNode = endNode.parentNode;
5343 // "Otherwise, if end node is a Text node and its parent's resolved
5344 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5345 // offset is not end node's length and the end offsetth element of
5346 // end node's data is a space (0x0020) or non-breaking space (0x00A0):"
5347 } else if (endNode.nodeType == Node.TEXT_NODE
5348 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
5349 && endOffset != getNodeLength(endNode)
5350 && /[ \xa0]/.test(endNode.data[endOffset])) {
5351 // "If fix collapsed space is true, and collapse spaces is true,
5352 // and the end offsetth code unit of end node's data is a space
5353 // (0x0020): call deleteData(end offset, 1) on end node, then
5354 // continue this loop from the beginning."
5355 if (fixCollapsedSpace
5356 && collapseSpaces
5357 && " " == endNode.data[endOffset]) {
5358 endNode.deleteData(endOffset, 1);
5359 continue;
5360 }
5362 // "Set collapse spaces to true if the end offsetth element of end
5363 // node's data is a space (0x0020), false otherwise."
5364 collapseSpaces = " " == endNode.data[endOffset];
5366 // "Add one to end offset."
5367 endOffset++;
5369 // "Add one to length."
5370 length++;
5372 // "Otherwise, break from this loop."
5373 } else {
5374 break;
5375 }
5376 }
5378 // "If fix collapsed space is true, then while (start node, start offset)
5379 // is before (end node, end offset):"
5380 if (fixCollapsedSpace) {
5381 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5382 // "If end node has a child in the same editing host with index end
5383 // offset − 1, set end node to that child, then set end offset to end
5384 // node's length."
5385 if (0 <= endOffset - 1
5386 && endOffset - 1 < endNode.childNodes.length
5387 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
5388 endNode = endNode.childNodes[endOffset - 1];
5389 endOffset = getNodeLength(endNode);
5391 // "Otherwise, if end offset is zero and end node's parent is in the
5392 // same editing host, set end offset to end node's index, then set end
5393 // node to its parent."
5394 } else if (endOffset == 0
5395 && inSameEditingHost(endNode, endNode.parentNode)) {
5396 endOffset = getNodeIndex(endNode);
5397 endNode = endNode.parentNode;
5399 // "Otherwise, if end node is a Text node and its parent's resolved
5400 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5401 // offset is end node's length and the last code unit of end node's
5402 // data is a space (0x0020) and end node precedes a line break:"
5403 } else if (endNode.nodeType == Node.TEXT_NODE
5404 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
5405 && endOffset == getNodeLength(endNode)
5406 && endNode.data[endNode.data.length - 1] == " "
5407 && precedesLineBreak(endNode)) {
5408 // "Subtract one from end offset."
5409 endOffset--;
5411 // "Subtract one from length."
5412 length--;
5414 // "Call deleteData(end offset, 1) on end node."
5415 endNode.deleteData(endOffset, 1);
5417 // "Otherwise, break from this loop."
5418 } else {
5419 break;
5420 }
5421 }
5422 }
5424 // "Let replacement whitespace be the canonical space sequence of length
5425 // length. non-breaking start is true if start offset is zero and start
5426 // node follows a line break, and false otherwise. non-breaking end is true
5427 // if end offset is end node's length and end node precedes a line break,
5428 // and false otherwise."
5429 var replacementWhitespace = canonicalSpaceSequence(length,
5430 startOffset == 0 && followsLineBreak(startNode),
5431 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
5433 // "While (start node, start offset) is before (end node, end offset):"
5434 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5435 // "If start node has a child with index start offset, set start node
5436 // to that child, then set start offset to zero."
5437 if (startOffset < startNode.childNodes.length) {
5438 startNode = startNode.childNodes[startOffset];
5439 startOffset = 0;
5441 // "Otherwise, if start node is not a Text node or if start offset is
5442 // start node's length, set start offset to one plus start node's
5443 // index, then set start node to its parent."
5444 } else if (startNode.nodeType != Node.TEXT_NODE
5445 || startOffset == getNodeLength(startNode)) {
5446 startOffset = 1 + getNodeIndex(startNode);
5447 startNode = startNode.parentNode;
5449 // "Otherwise:"
5450 } else {
5451 // "Remove the first element from replacement whitespace, and let
5452 // element be that element."
5453 var element = replacementWhitespace[0];
5454 replacementWhitespace = replacementWhitespace.slice(1);
5456 // "If element is not the same as the start offsetth element of
5457 // start node's data:"
5458 if (element != startNode.data[startOffset]) {
5459 // "Call insertData(start offset, element) on start node."
5460 startNode.insertData(startOffset, element);
5462 // "Call deleteData(start offset + 1, 1) on start node."
5463 startNode.deleteData(startOffset + 1, 1);
5464 }
5466 // "Add one to start offset."
5467 startOffset++;
5468 }
5469 }
5470 }
5473 //@}
5474 ///// Indenting and outdenting /////
5475 //@{
5477 function indentNodes(nodeList) {
5478 // "If node list is empty, do nothing and abort these steps."
5479 if (!nodeList.length) {
5480 return;
5481 }
5483 // "Let first node be the first member of node list."
5484 var firstNode = nodeList[0];
5486 // "If first node's parent is an ol or ul:"
5487 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
5488 // "Let tag be the local name of the parent of first node."
5489 var tag = firstNode.parentNode.tagName;
5491 // "Wrap node list, with sibling criteria returning true for an HTML
5492 // element with local name tag and false otherwise, and new parent
5493 // instructions returning the result of calling createElement(tag) on
5494 // the ownerDocument of first node."
5495 wrap(nodeList,
5496 function(node) { return isHtmlElement(node, tag) },
5497 function() { return firstNode.ownerDocument.createElement(tag) });
5499 // "Abort these steps."
5500 return;
5501 }
5503 // "Wrap node list, with sibling criteria returning true for a simple
5504 // indentation element and false otherwise, and new parent instructions
5505 // returning the result of calling createElement("blockquote") on the
5506 // ownerDocument of first node. Let new parent be the result."
5507 var newParent = wrap(nodeList,
5508 function(node) { return isSimpleIndentationElement(node) },
5509 function() { return firstNode.ownerDocument.createElement("blockquote") });
5511 // "Fix disallowed ancestors of new parent."
5512 fixDisallowedAncestors(newParent);
5513 }
5515 function outdentNode(node) {
5516 // "If node is not editable, abort these steps."
5517 if (!isEditable(node)) {
5518 return;
5519 }
5521 // "If node is a simple indentation element, remove node, preserving its
5522 // descendants. Then abort these steps."
5523 if (isSimpleIndentationElement(node)) {
5524 removePreservingDescendants(node);
5525 return;
5526 }
5528 // "If node is an indentation element:"
5529 if (isIndentationElement(node)) {
5530 // "Unset the dir attribute of node, if any."
5531 node.removeAttribute("dir");
5533 // "Unset the margin, padding, and border CSS properties of node."
5534 node.style.margin = "";
5535 node.style.padding = "";
5536 node.style.border = "";
5537 if (node.getAttribute("style") == ""
5538 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
5539 || node.getAttribute("style") == "border-width: initial; border-color: initial; ") {
5540 node.removeAttribute("style");
5541 }
5543 // "Set the tag name of node to "div"."
5544 setTagName(node, "div");
5546 // "Abort these steps."
5547 return;
5548 }
5550 // "Let current ancestor be node's parent."
5551 var currentAncestor = node.parentNode;
5553 // "Let ancestor list be a list of nodes, initially empty."
5554 var ancestorList = [];
5556 // "While current ancestor is an editable Element that is neither a simple
5557 // indentation element nor an ol nor a ul, append current ancestor to
5558 // ancestor list and then set current ancestor to its parent."
5559 while (isEditable(currentAncestor)
5560 && currentAncestor.nodeType == Node.ELEMENT_NODE
5561 && !isSimpleIndentationElement(currentAncestor)
5562 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5563 ancestorList.push(currentAncestor);
5564 currentAncestor = currentAncestor.parentNode;
5565 }
5567 // "If current ancestor is not an editable simple indentation element:"
5568 if (!isEditable(currentAncestor)
5569 || !isSimpleIndentationElement(currentAncestor)) {
5570 // "Let current ancestor be node's parent."
5571 currentAncestor = node.parentNode;
5573 // "Let ancestor list be the empty list."
5574 ancestorList = [];
5576 // "While current ancestor is an editable Element that is neither an
5577 // indentation element nor an ol nor a ul, append current ancestor to
5578 // ancestor list and then set current ancestor to its parent."
5579 while (isEditable(currentAncestor)
5580 && currentAncestor.nodeType == Node.ELEMENT_NODE
5581 && !isIndentationElement(currentAncestor)
5582 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5583 ancestorList.push(currentAncestor);
5584 currentAncestor = currentAncestor.parentNode;
5585 }
5586 }
5588 // "If node is an ol or ul and current ancestor is not an editable
5589 // indentation element:"
5590 if (isHtmlElement(node, ["OL", "UL"])
5591 && (!isEditable(currentAncestor)
5592 || !isIndentationElement(currentAncestor))) {
5593 // "Unset the reversed, start, and type attributes of node, if any are
5594 // set."
5595 node.removeAttribute("reversed");
5596 node.removeAttribute("start");
5597 node.removeAttribute("type");
5599 // "Let children be the children of node."
5600 var children = [].slice.call(node.childNodes);
5602 // "If node has attributes, and its parent is not an ol or ul, set the
5603 // tag name of node to "div"."
5604 if (node.attributes.length
5605 && !isHtmlElement(node.parentNode, ["OL", "UL"])) {
5606 setTagName(node, "div");
5608 // "Otherwise:"
5609 } else {
5610 // "Record the values of node's children, and let values be the
5611 // result."
5612 var values = recordValues([].slice.call(node.childNodes));
5614 // "Remove node, preserving its descendants."
5615 removePreservingDescendants(node);
5617 // "Restore the values from values."
5618 restoreValues(values);
5619 }
5621 // "Fix disallowed ancestors of each member of children."
5622 for (var i = 0; i < children.length; i++) {
5623 fixDisallowedAncestors(children[i]);
5624 }
5626 // "Abort these steps."
5627 return;
5628 }
5630 // "If current ancestor is not an editable indentation element, abort these
5631 // steps."
5632 if (!isEditable(currentAncestor)
5633 || !isIndentationElement(currentAncestor)) {
5634 return;
5635 }
5637 // "Append current ancestor to ancestor list."
5638 ancestorList.push(currentAncestor);
5640 // "Let original ancestor be current ancestor."
5641 var originalAncestor = currentAncestor;
5643 // "While ancestor list is not empty:"
5644 while (ancestorList.length) {
5645 // "Let current ancestor be the last member of ancestor list."
5646 //
5647 // "Remove the last member of ancestor list."
5648 currentAncestor = ancestorList.pop();
5650 // "Let target be the child of current ancestor that is equal to either
5651 // node or the last member of ancestor list."
5652 var target = node.parentNode == currentAncestor
5653 ? node
5654 : ancestorList[ancestorList.length - 1];
5656 // "If target is an inline node that is not a br, and its nextSibling
5657 // is a br, remove target's nextSibling from its parent."
5658 if (isInlineNode(target)
5659 && !isHtmlElement(target, "BR")
5660 && isHtmlElement(target.nextSibling, "BR")) {
5661 target.parentNode.removeChild(target.nextSibling);
5662 }
5664 // "Let preceding siblings be the preceding siblings of target, and let
5665 // following siblings be the following siblings of target."
5666 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target));
5667 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target));
5669 // "Indent preceding siblings."
5670 indentNodes(precedingSiblings);
5672 // "Indent following siblings."
5673 indentNodes(followingSiblings);
5674 }
5676 // "Outdent original ancestor."
5677 outdentNode(originalAncestor);
5678 }
5681 //@}
5682 ///// Toggling lists /////
5683 //@{
5685 function toggleLists(tagName) {
5686 // "Let mode be "disable" if the selection's list state is tag name, and
5687 // "enable" otherwise."
5688 var mode = getSelectionListState() == tagName ? "disable" : "enable";
5690 var range = getActiveRange();
5691 tagName = tagName.toUpperCase();
5693 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
5694 // "ol"."
5695 var otherTagName = tagName == "OL" ? "UL" : "OL";
5697 // "Let items be a list of all lis that are ancestor containers of the
5698 // range's start and/or end node."
5699 //
5700 // It's annoying to get this in tree order using functional stuff without
5701 // doing getDescendants(document), which is slow, so I do it imperatively.
5702 var items = [];
5703 (function(){
5704 for (
5705 var ancestorContainer = range.endContainer;
5706 ancestorContainer != range.commonAncestorContainer;
5707 ancestorContainer = ancestorContainer.parentNode
5708 ) {
5709 if (isHtmlElement(ancestorContainer, "li")) {
5710 items.unshift(ancestorContainer);
5711 }
5712 }
5713 for (
5714 var ancestorContainer = range.startContainer;
5715 ancestorContainer;
5716 ancestorContainer = ancestorContainer.parentNode
5717 ) {
5718 if (isHtmlElement(ancestorContainer, "li")) {
5719 items.unshift(ancestorContainer);
5720 }
5721 }
5722 })();
5724 // "For each item in items, normalize sublists of item."
5725 items.forEach(normalizeSublists);
5727 // "Block-extend the range, and let new range be the result."
5728 var newRange = blockExtend(range);
5730 // "If mode is "enable", then let lists to convert consist of every
5731 // editable HTML element with local name other tag name that is contained
5732 // in new range, and for every list in lists to convert:"
5733 if (mode == "enable") {
5734 getAllContainedNodes(newRange, function(node) {
5735 return isEditable(node)
5736 && isHtmlElement(node, otherTagName);
5737 }).forEach(function(list) {
5738 // "If list's previousSibling or nextSibling is an editable HTML
5739 // element with local name tag name:"
5740 if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName))
5741 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
5742 // "Let children be list's children."
5743 var children = [].slice.call(list.childNodes);
5745 // "Record the values of children, and let values be the
5746 // result."
5747 var values = recordValues(children);
5749 // "Split the parent of children."
5750 splitParent(children);
5752 // "Wrap children, with sibling criteria returning true for an
5753 // HTML element with local name tag name and false otherwise."
5754 wrap(children, function(node) { return isHtmlElement(node, tagName) });
5756 // "Restore the values from values."
5757 restoreValues(values);
5759 // "Otherwise, set the tag name of list to tag name."
5760 } else {
5761 setTagName(list, tagName);
5762 }
5763 });
5764 }
5766 // "Let node list be a list of nodes, initially empty."
5767 //
5768 // "For each node node contained in new range, if node is editable; the
5769 // last member of node list (if any) is not an ancestor of node; node
5770 // is not an indentation element; and either node is an ol or ul, or its
5771 // parent is an ol or ul, or it is an allowed child of "li"; then append
5772 // node to node list."
5773 var nodeList = getContainedNodes(newRange, function(node) {
5774 return isEditable(node)
5775 && !isIndentationElement(node)
5776 && (isHtmlElement(node, ["OL", "UL"])
5777 || isHtmlElement(node.parentNode, ["OL", "UL"])
5778 || isAllowedChild(node, "li"));
5779 });
5781 // "If mode is "enable", remove from node list any ol or ul whose parent is
5782 // not also an ol or ul."
5783 if (mode == "enable") {
5784 nodeList = nodeList.filter(function(node) {
5785 return !isHtmlElement(node, ["ol", "ul"])
5786 || isHtmlElement(node.parentNode, ["ol", "ul"]);
5787 });
5788 }
5790 // "If mode is "disable", then while node list is not empty:"
5791 if (mode == "disable") {
5792 while (nodeList.length) {
5793 // "Let sublist be an empty list of nodes."
5794 var sublist = [];
5796 // "Remove the first member from node list and append it to
5797 // sublist."
5798 sublist.push(nodeList.shift());
5800 // "If the first member of sublist is an HTML element with local
5801 // name tag name, outdent it and continue this loop from the
5802 // beginning."
5803 if (isHtmlElement(sublist[0], tagName)) {
5804 outdentNode(sublist[0]);
5805 continue;
5806 }
5808 // "While node list is not empty, and the first member of node list
5809 // is the nextSibling of the last member of sublist and is not an
5810 // HTML element with local name tag name, remove the first member
5811 // from node list and append it to sublist."
5812 while (nodeList.length
5813 && nodeList[0] == sublist[sublist.length - 1].nextSibling
5814 && !isHtmlElement(nodeList[0], tagName)) {
5815 sublist.push(nodeList.shift());
5816 }
5818 // "Record the values of sublist, and let values be the result."
5819 var values = recordValues(sublist);
5821 // "Split the parent of sublist."
5822 splitParent(sublist);
5824 // "Fix disallowed ancestors of each member of sublist."
5825 for (var i = 0; i < sublist.length; i++) {
5826 fixDisallowedAncestors(sublist[i]);
5827 }
5829 // "Restore the values from values."
5830 restoreValues(values);
5831 }
5833 // "Otherwise, while node list is not empty:"
5834 } else {
5835 while (nodeList.length) {
5836 // "Let sublist be an empty list of nodes."
5837 var sublist = [];
5839 // "While either sublist is empty, or node list is not empty and
5840 // its first member is the nextSibling of sublist's last member:"
5841 while (!sublist.length
5842 || (nodeList.length
5843 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
5844 // "If node list's first member is a p or div, set the tag name
5845 // of node list's first member to "li", and append the result
5846 // to sublist. Remove the first member from node list."
5847 if (isHtmlElement(nodeList[0], ["p", "div"])) {
5848 sublist.push(setTagName(nodeList[0], "li"));
5849 nodeList.shift();
5851 // "Otherwise, if the first member of node list is an li or ol
5852 // or ul, remove it from node list and append it to sublist."
5853 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
5854 sublist.push(nodeList.shift());
5856 // "Otherwise:"
5857 } else {
5858 // "Let nodes to wrap be a list of nodes, initially empty."
5859 var nodesToWrap = [];
5861 // "While nodes to wrap is empty, or node list is not empty
5862 // and its first member is the nextSibling of nodes to
5863 // wrap's last member and the first member of node list is
5864 // an inline node and the last member of nodes to wrap is
5865 // an inline node other than a br, remove the first member
5866 // from node list and append it to nodes to wrap."
5867 while (!nodesToWrap.length
5868 || (nodeList.length
5869 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
5870 && isInlineNode(nodeList[0])
5871 && isInlineNode(nodesToWrap[nodesToWrap.length - 1])
5872 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
5873 nodesToWrap.push(nodeList.shift());
5874 }
5876 // "Wrap nodes to wrap, with new parent instructions
5877 // returning the result of calling createElement("li") on
5878 // the context object. Append the result to sublist."
5879 sublist.push(wrap(nodesToWrap,
5880 undefined,
5881 function() { return document.createElement("li") }));
5882 }
5883 }
5885 // "If sublist's first member's parent is an HTML element with
5886 // local name tag name, or if every member of sublist is an ol or
5887 // ul, continue this loop from the beginning."
5888 if (isHtmlElement(sublist[0].parentNode, tagName)
5889 || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
5890 continue;
5891 }
5893 // "If sublist's first member's parent is an HTML element with
5894 // local name other tag name:"
5895 if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
5896 // "Record the values of sublist, and let values be the
5897 // result."
5898 var values = recordValues(sublist);
5900 // "Split the parent of sublist."
5901 splitParent(sublist);
5903 // "Wrap sublist, with sibling criteria returning true for an
5904 // HTML element with local name tag name and false otherwise,
5905 // and new parent instructions returning the result of calling
5906 // createElement(tag name) on the context object."
5907 wrap(sublist,
5908 function(node) { return isHtmlElement(node, tagName) },
5909 function() { return document.createElement(tagName) });
5911 // "Restore the values from values."
5912 restoreValues(values);
5914 // "Continue this loop from the beginning."
5915 continue;
5916 }
5918 // "Wrap sublist, with sibling criteria returning true for an HTML
5919 // element with local name tag name and false otherwise, and new
5920 // parent instructions being the following:"
5921 // . . .
5922 // "Fix disallowed ancestors of the previous step's result."
5923 fixDisallowedAncestors(wrap(sublist,
5924 function(node) { return isHtmlElement(node, tagName) },
5925 function() {
5926 // "If sublist's first member's parent is not an editable
5927 // simple indentation element, or sublist's first member's
5928 // parent's previousSibling is not an editable HTML element
5929 // with local name tag name, call createElement(tag name)
5930 // on the context object and return the result."
5931 if (!isEditable(sublist[0].parentNode)
5932 || !isSimpleIndentationElement(sublist[0].parentNode)
5933 || !isEditable(sublist[0].parentNode.previousSibling)
5934 || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) {
5935 return document.createElement(tagName);
5936 }
5938 // "Let list be sublist's first member's parent's
5939 // previousSibling."
5940 var list = sublist[0].parentNode.previousSibling;
5942 // "Normalize sublists of list's lastChild."
5943 normalizeSublists(list.lastChild);
5945 // "If list's lastChild is not an editable HTML element
5946 // with local name tag name, call createElement(tag name)
5947 // on the context object, and append the result as the last
5948 // child of list."
5949 if (!isEditable(list.lastChild)
5950 || !isHtmlElement(list.lastChild, tagName)) {
5951 list.appendChild(document.createElement(tagName));
5952 }
5954 // "Return the last child of list."
5955 return list.lastChild;
5956 }
5957 ));
5958 }
5959 }
5960 }
5963 //@}
5964 ///// Justifying the selection /////
5965 //@{
5967 function justifySelection(alignment) {
5968 // "Block-extend the active range, and let new range be the result."
5969 var newRange = blockExtend(globalRange);
5971 // "Let element list be a list of all editable Elements contained in new
5972 // range that either has an attribute in the HTML namespace whose local
5973 // name is "align", or has a style attribute that sets "text-align", or is
5974 // a center."
5975 var elementList = getAllContainedNodes(newRange, function(node) {
5976 return node.nodeType == Node.ELEMENT_NODE
5977 && isEditable(node)
5978 // Ignoring namespaces here
5979 && (
5980 node.hasAttribute("align")
5981 || node.style.textAlign != ""
5982 || isHtmlElement(node, "center")
5983 );
5984 });
5986 // "For each element in element list:"
5987 for (var i = 0; i < elementList.length; i++) {
5988 var element = elementList[i];
5990 // "If element has an attribute in the HTML namespace whose local name
5991 // is "align", remove that attribute."
5992 element.removeAttribute("align");
5994 // "Unset the CSS property "text-align" on element, if it's set by a
5995 // style attribute."
5996 element.style.textAlign = "";
5997 if (element.getAttribute("style") == "") {
5998 element.removeAttribute("style");
5999 }
6001 // "If element is a div or span or center with no attributes, remove
6002 // it, preserving its descendants."
6003 if (isHtmlElement(element, ["div", "span", "center"])
6004 && !element.attributes.length) {
6005 removePreservingDescendants(element);
6006 }
6008 // "If element is a center with one or more attributes, set the tag
6009 // name of element to "div"."
6010 if (isHtmlElement(element, "center")
6011 && element.attributes.length) {
6012 setTagName(element, "div");
6013 }
6014 }
6016 // "Block-extend the active range, and let new range be the result."
6017 newRange = blockExtend(globalRange);
6019 // "Let node list be a list of nodes, initially empty."
6020 var nodeList = [];
6022 // "For each node node contained in new range, append node to node list if
6023 // the last member of node list (if any) is not an ancestor of node; node
6024 // is editable; node is an allowed child of "div"; and node's alignment
6025 // value is not alignment."
6026 nodeList = getContainedNodes(newRange, function(node) {
6027 return isEditable(node)
6028 && isAllowedChild(node, "div")
6029 && getAlignmentValue(node) != alignment;
6030 });
6032 // "While node list is not empty:"
6033 while (nodeList.length) {
6034 // "Let sublist be a list of nodes, initially empty."
6035 var sublist = [];
6037 // "Remove the first member of node list and append it to sublist."
6038 sublist.push(nodeList.shift());
6040 // "While node list is not empty, and the first member of node list is
6041 // the nextSibling of the last member of sublist, remove the first
6042 // member of node list and append it to sublist."
6043 while (nodeList.length
6044 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
6045 sublist.push(nodeList.shift());
6046 }
6048 // "Wrap sublist. Sibling criteria returns true for any div that has
6049 // one or both of the following two attributes and no other attributes,
6050 // and false otherwise:"
6051 //
6052 // * "An align attribute whose value is an ASCII case-insensitive
6053 // match for alignment.
6054 // * "A style attribute which sets exactly one CSS property
6055 // (including unrecognized or invalid attributes), which is
6056 // "text-align", which is set to alignment.
6057 //
6058 // "New parent instructions are to call createElement("div") on the
6059 // context object, then set its CSS property "text-align" to alignment
6060 // and return the result."
6061 wrap(sublist,
6062 function(node) {
6063 return isHtmlElement(node, "div")
6064 && [].every.call(node.attributes, function(attr) {
6065 return (attr.name == "align" && attr.value.toLowerCase() == alignment)
6066 || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment);
6067 });
6068 },
6069 function() {
6070 var newParent = document.createElement("div");
6071 newParent.setAttribute("style", "text-align: " + alignment);
6072 return newParent;
6073 }
6074 );
6075 }
6076 }
6079 //@}
6080 ///// Automatic linking /////
6081 //@{
6082 // "An autolinkable URL is a string of the following form:"
6083 var autolinkableUrlRegexp =
6084 // "Either a string matching the scheme pattern from RFC 3986 section 3.1
6085 // followed by the literal string ://, or the literal string mailto:;
6086 // followed by"
6087 //
6088 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
6089 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
6090 // "Zero or more characters other than space characters; followed by"
6091 + "[^ \t\n\f\r]*"
6092 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
6093 + "[^!\"'(),\\-.:;<>[\\]`{}]";
6095 // "A valid e-mail address is a string that matches the ABNF production 1*(
6096 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
6097 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
6098 //
6099 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
6100 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
6101 //
6102 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
6103 //<let-dig-hyp> ::= <let-dig> | "-"
6104 //<let-dig> ::= <letter> | <digit>
6105 var validEmailRegexp =
6106 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
6108 function autolink(node, endOffset) {
6109 // "While (node, end offset)'s previous equivalent point is not null, set
6110 // it to its previous equivalent point."
6111 while (getPreviousEquivalentPoint(node, endOffset)) {
6112 var prev = getPreviousEquivalentPoint(node, endOffset);
6113 node = prev[0];
6114 endOffset = prev[1];
6115 }
6117 // "If node is not a Text node, or has an a ancestor, do nothing and abort
6118 // these steps."
6119 if (node.nodeType != Node.TEXT_NODE
6120 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) {
6121 return;
6122 }
6124 // "Let search be the largest substring of node's data whose end is end
6125 // offset and that contains no space characters."
6126 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
6128 // "If some substring of search is an autolinkable URL:"
6129 if (new RegExp(autolinkableUrlRegexp).test(search)) {
6130 // "While there is no substring of node's data ending at end offset
6131 // that is an autolinkable URL, decrement end offset."
6132 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) {
6133 endOffset--;
6134 }
6136 // "Let start offset be the start index of the longest substring of
6137 // node's data that is an autolinkable URL ending at end offset."
6138 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index;
6140 // "Let href be the substring of node's data starting at start offset
6141 // and ending at end offset."
6142 var href = node.substringData(startOffset, endOffset - startOffset);
6144 // "Otherwise, if some substring of search is a valid e-mail address:"
6145 } else if (new RegExp(validEmailRegexp).test(search)) {
6146 // "While there is no substring of node's data ending at end offset
6147 // that is a valid e-mail address, decrement end offset."
6148 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) {
6149 endOffset--;
6150 }
6152 // "Let start offset be the start index of the longest substring of
6153 // node's data that is a valid e-mail address ending at end offset."
6154 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index;
6156 // "Let href be "mailto:" concatenated with the substring of node's
6157 // data starting at start offset and ending at end offset."
6158 var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset);
6160 // "Otherwise, do nothing and abort these steps."
6161 } else {
6162 return;
6163 }
6165 // "Let original range be the active range."
6166 var originalRange = getActiveRange();
6168 // "Create a new range with start (node, start offset) and end (node, end
6169 // offset), and set the context object's selection's range to it."
6170 var newRange = document.createRange();
6171 newRange.setStart(node, startOffset);
6172 newRange.setEnd(node, endOffset);
6173 getSelection().removeAllRanges();
6174 getSelection().addRange(newRange);
6175 globalRange = newRange;
6177 // "Take the action for "createLink", with value equal to href."
6178 commands.createlink.action(href);
6180 // "Set the context object's selection's range to original range."
6181 getSelection().removeAllRanges();
6182 getSelection().addRange(originalRange);
6183 globalRange = originalRange;
6184 }
6185 //@}
6186 ///// The delete command /////
6187 //@{
6188 commands["delete"] = {
6189 preservesOverrides: true,
6190 action: function() {
6191 // "If the active range is not collapsed, delete the selection and
6192 // return true."
6193 if (!getActiveRange().collapsed) {
6194 deleteSelection();
6195 return true;
6196 }
6198 // "Canonicalize whitespace at the active range's start."
6199 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
6201 // "Let node and offset be the active range's start node and offset."
6202 var node = getActiveRange().startContainer;
6203 var offset = getActiveRange().startOffset;
6205 // "Repeat the following steps:"
6206 while (true) {
6207 // "If offset is zero and node's previousSibling is an editable
6208 // invisible node, remove node's previousSibling from its parent."
6209 if (offset == 0
6210 && isEditable(node.previousSibling)
6211 && isInvisible(node.previousSibling)) {
6212 node.parentNode.removeChild(node.previousSibling);
6214 // "Otherwise, if node has a child with index offset − 1 and that
6215 // child is an editable invisible node, remove that child from
6216 // node, then subtract one from offset."
6217 } else if (0 <= offset - 1
6218 && offset - 1 < node.childNodes.length
6219 && isEditable(node.childNodes[offset - 1])
6220 && isInvisible(node.childNodes[offset - 1])) {
6221 node.removeChild(node.childNodes[offset - 1]);
6222 offset--;
6224 // "Otherwise, if offset is zero and node is an inline node, or if
6225 // node is an invisible node, set offset to the index of node, then
6226 // set node to its parent."
6227 } else if ((offset == 0
6228 && isInlineNode(node))
6229 || isInvisible(node)) {
6230 offset = getNodeIndex(node);
6231 node = node.parentNode;
6233 // "Otherwise, if node has a child with index offset − 1 and that
6234 // child is an editable a, remove that child from node, preserving
6235 // its descendants. Then return true."
6236 } else if (0 <= offset - 1
6237 && offset - 1 < node.childNodes.length
6238 && isEditable(node.childNodes[offset - 1])
6239 && isHtmlElement(node.childNodes[offset - 1], "a")) {
6240 removePreservingDescendants(node.childNodes[offset - 1]);
6241 return true;
6243 // "Otherwise, if node has a child with index offset − 1 and that
6244 // child is not a block node or a br or an img, set node to that
6245 // child, then set offset to the length of node."
6246 } else if (0 <= offset - 1
6247 && offset - 1 < node.childNodes.length
6248 && !isBlockNode(node.childNodes[offset - 1])
6249 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
6250 node = node.childNodes[offset - 1];
6251 offset = getNodeLength(node);
6253 // "Otherwise, break from this loop."
6254 } else {
6255 break;
6256 }
6257 }
6259 // "If node is a Text node and offset is not zero, or if node is a
6260 // block node that has a child with index offset − 1 and that child is
6261 // a br or hr or img:"
6262 if ((node.nodeType == Node.TEXT_NODE
6263 && offset != 0)
6264 || (isBlockNode(node)
6265 && 0 <= offset - 1
6266 && offset - 1 < node.childNodes.length
6267 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
6268 // "Call collapse(node, offset) on the context object's Selection."
6269 getSelection().collapse(node, offset);
6270 getActiveRange().setEnd(node, offset);
6272 // "Call extend(node, offset − 1) on the context object's
6273 // Selection."
6274 getSelection().extend(node, offset - 1);
6275 getActiveRange().setStart(node, offset - 1);
6277 // "Delete the selection."
6278 deleteSelection();
6280 // "Return true."
6281 return true;
6282 }
6284 // "If node is an inline node, return true."
6285 if (isInlineNode(node)) {
6286 return true;
6287 }
6289 // "If node is an li or dt or dd and is the first child of its parent,
6290 // and offset is zero:"
6291 if (isHtmlElement(node, ["li", "dt", "dd"])
6292 && node == node.parentNode.firstChild
6293 && offset == 0) {
6294 // "Let items be a list of all lis that are ancestors of node."
6295 //
6296 // Remember, must be in tree order.
6297 var items = [];
6298 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
6299 if (isHtmlElement(ancestor, "li")) {
6300 items.unshift(ancestor);
6301 }
6302 }
6304 // "Normalize sublists of each item in items."
6305 for (var i = 0; i < items.length; i++) {
6306 normalizeSublists(items[i]);
6307 }
6309 // "Record the values of the one-node list consisting of node, and
6310 // let values be the result."
6311 var values = recordValues([node]);
6313 // "Split the parent of the one-node list consisting of node."
6314 splitParent([node]);
6316 // "Restore the values from values."
6317 restoreValues(values);
6319 // "If node is a dd or dt, and it is not an allowed child of any of
6320 // its ancestors in the same editing host, set the tag name of node
6321 // to the default single-line container name and let node be the
6322 // result."
6323 if (isHtmlElement(node, ["dd", "dt"])
6324 && getAncestors(node).every(function(ancestor) {
6325 return !inSameEditingHost(node, ancestor)
6326 || !isAllowedChild(node, ancestor)
6327 })) {
6328 node = setTagName(node, defaultSingleLineContainerName);
6329 }
6331 // "Fix disallowed ancestors of node."
6332 fixDisallowedAncestors(node);
6334 // "Return true."
6335 return true;
6336 }
6338 // "Let start node equal node and let start offset equal offset."
6339 var startNode = node;
6340 var startOffset = offset;
6342 // "Repeat the following steps:"
6343 while (true) {
6344 // "If start offset is zero, set start offset to the index of start
6345 // node and then set start node to its parent."
6346 if (startOffset == 0) {
6347 startOffset = getNodeIndex(startNode);
6348 startNode = startNode.parentNode;
6350 // "Otherwise, if start node has an editable invisible child with
6351 // index start offset minus one, remove it from start node and
6352 // subtract one from start offset."
6353 } else if (0 <= startOffset - 1
6354 && startOffset - 1 < startNode.childNodes.length
6355 && isEditable(startNode.childNodes[startOffset - 1])
6356 && isInvisible(startNode.childNodes[startOffset - 1])) {
6357 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6358 startOffset--;
6360 // "Otherwise, break from this loop."
6361 } else {
6362 break;
6363 }
6364 }
6366 // "If offset is zero, and node has an editable ancestor container in
6367 // the same editing host that's an indentation element:"
6368 if (offset == 0
6369 && getAncestors(node).concat(node).filter(function(ancestor) {
6370 return isEditable(ancestor)
6371 && inSameEditingHost(ancestor, node)
6372 && isIndentationElement(ancestor);
6373 }).length) {
6374 // "Block-extend the range whose start and end are both (node, 0),
6375 // and let new range be the result."
6376 var newRange = document.createRange();
6377 newRange.setStart(node, 0);
6378 newRange = blockExtend(newRange);
6380 // "Let node list be a list of nodes, initially empty."
6381 //
6382 // "For each node current node contained in new range, append
6383 // current node to node list if the last member of node list (if
6384 // any) is not an ancestor of current node, and current node is
6385 // editable but has no editable descendants."
6386 var nodeList = getContainedNodes(newRange, function(currentNode) {
6387 return isEditable(currentNode)
6388 && !hasEditableDescendants(currentNode);
6389 });
6391 // "Outdent each node in node list."
6392 for (var i = 0; i < nodeList.length; i++) {
6393 outdentNode(nodeList[i]);
6394 }
6396 // "Return true."
6397 return true;
6398 }
6400 // "If the child of start node with index start offset is a table,
6401 // return true."
6402 if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
6403 return true;
6404 }
6406 // "If start node has a child with index start offset − 1, and that
6407 // child is a table:"
6408 if (0 <= startOffset - 1
6409 && startOffset - 1 < startNode.childNodes.length
6410 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
6411 // "Call collapse(start node, start offset − 1) on the context
6412 // object's Selection."
6413 getSelection().collapse(startNode, startOffset - 1);
6414 getActiveRange().setStart(startNode, startOffset - 1);
6416 // "Call extend(start node, start offset) on the context object's
6417 // Selection."
6418 getSelection().extend(startNode, startOffset);
6419 getActiveRange().setEnd(startNode, startOffset);
6421 // "Return true."
6422 return true;
6423 }
6425 // "If offset is zero; and either the child of start node with index
6426 // start offset minus one is an hr, or the child is a br whose
6427 // previousSibling is either a br or not an inline node:"
6428 if (offset == 0
6429 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
6430 || (
6431 isHtmlElement(startNode.childNodes[startOffset - 1], "br")
6432 && (
6433 isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
6434 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
6435 )
6436 )
6437 )) {
6438 // "Call collapse(start node, start offset − 1) on the context
6439 // object's Selection."
6440 getSelection().collapse(startNode, startOffset - 1);
6441 getActiveRange().setStart(startNode, startOffset - 1);
6443 // "Call extend(start node, start offset) on the context object's
6444 // Selection."
6445 getSelection().extend(startNode, startOffset);
6446 getActiveRange().setEnd(startNode, startOffset);
6448 // "Delete the selection."
6449 deleteSelection();
6451 // "Call collapse(node, offset) on the Selection."
6452 getSelection().collapse(node, offset);
6453 getActiveRange().setStart(node, offset);
6454 getActiveRange().collapse(true);
6456 // "Return true."
6457 return true;
6458 }
6460 // "If the child of start node with index start offset is an li or dt
6461 // or dd, and that child's firstChild is an inline node, and start
6462 // offset is not zero:"
6463 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6464 && isInlineNode(startNode.childNodes[startOffset].firstChild)
6465 && startOffset != 0) {
6466 // "Let previous item be the child of start node with index start
6467 // offset minus one."
6468 var previousItem = startNode.childNodes[startOffset - 1];
6470 // "If previous item's lastChild is an inline node other than a br,
6471 // call createElement("br") on the context object and append the
6472 // result as the last child of previous item."
6473 if (isInlineNode(previousItem.lastChild)
6474 && !isHtmlElement(previousItem.lastChild, "br")) {
6475 previousItem.appendChild(document.createElement("br"));
6476 }
6478 // "If previous item's lastChild is an inline node, call
6479 // createElement("br") on the context object and append the result
6480 // as the last child of previous item."
6481 if (isInlineNode(previousItem.lastChild)) {
6482 previousItem.appendChild(document.createElement("br"));
6483 }
6484 }
6486 // "If start node's child with index start offset is an li or dt or dd,
6487 // and that child's previousSibling is also an li or dt or dd:"
6488 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6489 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) {
6490 // "Call cloneRange() on the active range, and let original range
6491 // be the result."
6492 //
6493 // We need to add it to extraRanges so it will actually get updated
6494 // when moving preserving ranges.
6495 var originalRange = getActiveRange().cloneRange();
6496 extraRanges.push(originalRange);
6498 // "Set start node to its child with index start offset − 1."
6499 startNode = startNode.childNodes[startOffset - 1];
6501 // "Set start offset to start node's length."
6502 startOffset = getNodeLength(startNode);
6504 // "Set node to start node's nextSibling."
6505 node = startNode.nextSibling;
6507 // "Call collapse(start node, start offset) on the context object's
6508 // Selection."
6509 getSelection().collapse(startNode, startOffset);
6510 getActiveRange().setStart(startNode, startOffset);
6512 // "Call extend(node, 0) on the context object's Selection."
6513 getSelection().extend(node, 0);
6514 getActiveRange().setEnd(node, 0);
6516 // "Delete the selection."
6517 deleteSelection();
6519 // "Call removeAllRanges() on the context object's Selection."
6520 getSelection().removeAllRanges();
6522 // "Call addRange(original range) on the context object's
6523 // Selection."
6524 getSelection().addRange(originalRange);
6525 getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset);
6526 getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset);
6528 // "Return true."
6529 extraRanges.pop();
6530 return true;
6531 }
6533 // "While start node has a child with index start offset minus one:"
6534 while (0 <= startOffset - 1
6535 && startOffset - 1 < startNode.childNodes.length) {
6536 // "If start node's child with index start offset minus one is
6537 // editable and invisible, remove it from start node, then subtract
6538 // one from start offset."
6539 if (isEditable(startNode.childNodes[startOffset - 1])
6540 && isInvisible(startNode.childNodes[startOffset - 1])) {
6541 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6542 startOffset--;
6544 // "Otherwise, set start node to its child with index start offset
6545 // minus one, then set start offset to the length of start node."
6546 } else {
6547 startNode = startNode.childNodes[startOffset - 1];
6548 startOffset = getNodeLength(startNode);
6549 }
6550 }
6552 // "Call collapse(start node, start offset) on the context object's
6553 // Selection."
6554 getSelection().collapse(startNode, startOffset);
6555 getActiveRange().setStart(startNode, startOffset);
6557 // "Call extend(node, offset) on the context object's Selection."
6558 getSelection().extend(node, offset);
6559 getActiveRange().setEnd(node, offset);
6561 // "Delete the selection, with direction "backward"."
6562 deleteSelection({direction: "backward"});
6564 // "Return true."
6565 return true;
6566 }
6567 };
6569 //@}
6570 ///// The formatBlock command /////
6571 //@{
6572 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
6573 // "h4", "h5", "h6", "p", or "pre"."
6574 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
6575 "h4", "h5", "h6", "p", "pre"];
6577 commands.formatblock = {
6578 preservesOverrides: true,
6579 action: function(value) {
6580 // "If value begins with a "<" character and ends with a ">" character,
6581 // remove the first and last characters from it."
6582 if (/^<.*>$/.test(value)) {
6583 value = value.slice(1, -1);
6584 }
6586 // "Let value be converted to ASCII lowercase."
6587 value = value.toLowerCase();
6589 // "If value is not a formattable block name, return false."
6590 if (formattableBlockNames.indexOf(value) == -1) {
6591 return false;
6592 }
6594 // "Block-extend the active range, and let new range be the result."
6595 var newRange = blockExtend(getActiveRange());
6597 // "Let node list be an empty list of nodes."
6598 //
6599 // "For each node node contained in new range, append node to node list
6600 // if it is editable, the last member of original node list (if any) is
6601 // not an ancestor of node, node is either a non-list single-line
6602 // container or an allowed child of "p" or a dd or dt, and node is not
6603 // the ancestor of a prohibited paragraph child."
6604 var nodeList = getContainedNodes(newRange, function(node) {
6605 return isEditable(node)
6606 && (isNonListSingleLineContainer(node)
6607 || isAllowedChild(node, "p")
6608 || isHtmlElement(node, ["dd", "dt"]))
6609 && !getDescendants(node).some(isProhibitedParagraphChild);
6610 });
6612 // "Record the values of node list, and let values be the result."
6613 var values = recordValues(nodeList);
6615 // "For each node in node list, while node is the descendant of an
6616 // editable HTML element in the same editing host, whose local name is
6617 // a formattable block name, and which is not the ancestor of a
6618 // prohibited paragraph child, split the parent of the one-node list
6619 // consisting of node."
6620 for (var i = 0; i < nodeList.length; i++) {
6621 var node = nodeList[i];
6622 while (getAncestors(node).some(function(ancestor) {
6623 return isEditable(ancestor)
6624 && inSameEditingHost(ancestor, node)
6625 && isHtmlElement(ancestor, formattableBlockNames)
6626 && !getDescendants(ancestor).some(isProhibitedParagraphChild);
6627 })) {
6628 splitParent([node]);
6629 }
6630 }
6632 // "Restore the values from values."
6633 restoreValues(values);
6635 // "While node list is not empty:"
6636 while (nodeList.length) {
6637 var sublist;
6639 // "If the first member of node list is a single-line
6640 // container:"
6641 if (isSingleLineContainer(nodeList[0])) {
6642 // "Let sublist be the children of the first member of node
6643 // list."
6644 sublist = [].slice.call(nodeList[0].childNodes);
6646 // "Record the values of sublist, and let values be the
6647 // result."
6648 var values = recordValues(sublist);
6650 // "Remove the first member of node list from its parent,
6651 // preserving its descendants."
6652 removePreservingDescendants(nodeList[0]);
6654 // "Restore the values from values."
6655 restoreValues(values);
6657 // "Remove the first member from node list."
6658 nodeList.shift();
6660 // "Otherwise:"
6661 } else {
6662 // "Let sublist be an empty list of nodes."
6663 sublist = [];
6665 // "Remove the first member of node list and append it to
6666 // sublist."
6667 sublist.push(nodeList.shift());
6669 // "While node list is not empty, and the first member of
6670 // node list is the nextSibling of the last member of
6671 // sublist, and the first member of node list is not a
6672 // single-line container, and the last member of sublist is
6673 // not a br, remove the first member of node list and
6674 // append it to sublist."
6675 while (nodeList.length
6676 && nodeList[0] == sublist[sublist.length - 1].nextSibling
6677 && !isSingleLineContainer(nodeList[0])
6678 && !isHtmlElement(sublist[sublist.length - 1], "BR")) {
6679 sublist.push(nodeList.shift());
6680 }
6681 }
6683 // "Wrap sublist. If value is "div" or "p", sibling criteria
6684 // returns false; otherwise it returns true for an HTML element
6685 // with local name value and no attributes, and false otherwise.
6686 // New parent instructions return the result of running
6687 // createElement(value) on the context object. Then fix disallowed
6688 // ancestors of the result."
6689 fixDisallowedAncestors(wrap(sublist,
6690 ["div", "p"].indexOf(value) == - 1
6691 ? function(node) { return isHtmlElement(node, value) && !node.attributes.length }
6692 : function() { return false },
6693 function() { return document.createElement(value) }));
6694 }
6696 // "Return true."
6697 return true;
6698 }, indeterm: function() {
6699 // "If the active range is null, return false."
6700 if (!getActiveRange()) {
6701 return false;
6702 }
6704 // "Block-extend the active range, and let new range be the result."
6705 var newRange = blockExtend(getActiveRange());
6707 // "Let node list be all visible editable nodes that are contained in
6708 // new range and have no children."
6709 var nodeList = getAllContainedNodes(newRange, function(node) {
6710 return isVisible(node)
6711 && isEditable(node)
6712 && !node.hasChildNodes();
6713 });
6715 // "If node list is empty, return false."
6716 if (!nodeList.length) {
6717 return false;
6718 }
6720 // "Let type be null."
6721 var type = null;
6723 // "For each node in node list:"
6724 for (var i = 0; i < nodeList.length; i++) {
6725 var node = nodeList[i];
6727 // "While node's parent is editable and in the same editing host as
6728 // node, and node is not an HTML element whose local name is a
6729 // formattable block name, set node to its parent."
6730 while (isEditable(node.parentNode)
6731 && inSameEditingHost(node, node.parentNode)
6732 && !isHtmlElement(node, formattableBlockNames)) {
6733 node = node.parentNode;
6734 }
6736 // "Let current type be the empty string."
6737 var currentType = "";
6739 // "If node is an editable HTML element whose local name is a
6740 // formattable block name, and node is not the ancestor of a
6741 // prohibited paragraph child, set current type to node's local
6742 // name."
6743 if (isEditable(node)
6744 && isHtmlElement(node, formattableBlockNames)
6745 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6746 currentType = node.tagName;
6747 }
6749 // "If type is null, set type to current type."
6750 if (type === null) {
6751 type = currentType;
6753 // "Otherwise, if type does not equal current type, return true."
6754 } else if (type != currentType) {
6755 return true;
6756 }
6757 }
6759 // "Return false."
6760 return false;
6761 }, value: function() {
6762 // "If the active range is null, return the empty string."
6763 if (!getActiveRange()) {
6764 return "";
6765 }
6767 // "Block-extend the active range, and let new range be the result."
6768 var newRange = blockExtend(getActiveRange());
6770 // "Let node be the first visible editable node that is contained in
6771 // new range and has no children. If there is no such node, return the
6772 // empty string."
6773 var nodes = getAllContainedNodes(newRange, function(node) {
6774 return isVisible(node)
6775 && isEditable(node)
6776 && !node.hasChildNodes();
6777 });
6778 if (!nodes.length) {
6779 return "";
6780 }
6781 var node = nodes[0];
6783 // "While node's parent is editable and in the same editing host as
6784 // node, and node is not an HTML element whose local name is a
6785 // formattable block name, set node to its parent."
6786 while (isEditable(node.parentNode)
6787 && inSameEditingHost(node, node.parentNode)
6788 && !isHtmlElement(node, formattableBlockNames)) {
6789 node = node.parentNode;
6790 }
6792 // "If node is an editable HTML element whose local name is a
6793 // formattable block name, and node is not the ancestor of a prohibited
6794 // paragraph child, return node's local name, converted to ASCII
6795 // lowercase."
6796 if (isEditable(node)
6797 && isHtmlElement(node, formattableBlockNames)
6798 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6799 return node.tagName.toLowerCase();
6800 }
6802 // "Return the empty string."
6803 return "";
6804 }
6805 };
6807 //@}
6808 ///// The forwardDelete command /////
6809 //@{
6810 commands.forwarddelete = {
6811 preservesOverrides: true,
6812 action: function() {
6813 // "If the active range is not collapsed, delete the selection and
6814 // return true."
6815 if (!getActiveRange().collapsed) {
6816 deleteSelection();
6817 return true;
6818 }
6820 // "Canonicalize whitespace at the active range's start."
6821 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
6823 // "Let node and offset be the active range's start node and offset."
6824 var node = getActiveRange().startContainer;
6825 var offset = getActiveRange().startOffset;
6827 // "Repeat the following steps:"
6828 while (true) {
6829 // "If offset is the length of node and node's nextSibling is an
6830 // editable invisible node, remove node's nextSibling from its
6831 // parent."
6832 if (offset == getNodeLength(node)
6833 && isEditable(node.nextSibling)
6834 && isInvisible(node.nextSibling)) {
6835 node.parentNode.removeChild(node.nextSibling);
6837 // "Otherwise, if node has a child with index offset and that child
6838 // is an editable invisible node, remove that child from node."
6839 } else if (offset < node.childNodes.length
6840 && isEditable(node.childNodes[offset])
6841 && isInvisible(node.childNodes[offset])) {
6842 node.removeChild(node.childNodes[offset]);
6844 // "Otherwise, if offset is the length of node and node is an
6845 // inline node, or if node is invisible, set offset to one plus the
6846 // index of node, then set node to its parent."
6847 } else if ((offset == getNodeLength(node)
6848 && isInlineNode(node))
6849 || isInvisible(node)) {
6850 offset = 1 + getNodeIndex(node);
6851 node = node.parentNode;
6853 // "Otherwise, if node has a child with index offset and that child
6854 // is neither a block node nor a br nor an img nor a collapsed
6855 // block prop, set node to that child, then set offset to zero."
6856 } else if (offset < node.childNodes.length
6857 && !isBlockNode(node.childNodes[offset])
6858 && !isHtmlElement(node.childNodes[offset], ["br", "img"])
6859 && !isCollapsedBlockProp(node.childNodes[offset])) {
6860 node = node.childNodes[offset];
6861 offset = 0;
6863 // "Otherwise, break from this loop."
6864 } else {
6865 break;
6866 }
6867 }
6869 // "If node is a Text node and offset is not node's length:"
6870 if (node.nodeType == Node.TEXT_NODE
6871 && offset != getNodeLength(node)) {
6872 // "Let end offset be offset plus one."
6873 var endOffset = offset + 1;
6875 // "While end offset is not node's length and the end offsetth
6876 // element of node's data has general category M when interpreted
6877 // as a Unicode code point, add one to end offset."
6878 //
6879 // TODO: Not even going to try handling anything beyond the most
6880 // basic combining marks, since I couldn't find a good list. I
6881 // special-case a few Hebrew diacritics too to test basic coverage
6882 // of non-Latin stuff.
6883 while (endOffset != node.length
6884 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
6885 endOffset++;
6886 }
6888 // "Call collapse(node, offset) on the context object's Selection."
6889 getSelection().collapse(node, offset);
6890 getActiveRange().setStart(node, offset);
6892 // "Call extend(node, end offset) on the context object's
6893 // Selection."
6894 getSelection().extend(node, endOffset);
6895 getActiveRange().setEnd(node, endOffset);
6897 // "Delete the selection."
6898 deleteSelection();
6900 // "Return true."
6901 return true;
6902 }
6904 // "If node is an inline node, return true."
6905 if (isInlineNode(node)) {
6906 return true;
6907 }
6909 // "If node has a child with index offset and that child is a br or hr
6910 // or img, but is not a collapsed block prop:"
6911 if (offset < node.childNodes.length
6912 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
6913 && !isCollapsedBlockProp(node.childNodes[offset])) {
6914 // "Call collapse(node, offset) on the context object's Selection."
6915 getSelection().collapse(node, offset);
6916 getActiveRange().setStart(node, offset);
6918 // "Call extend(node, offset + 1) on the context object's
6919 // Selection."
6920 getSelection().extend(node, offset + 1);
6921 getActiveRange().setEnd(node, offset + 1);
6923 // "Delete the selection."
6924 deleteSelection();
6926 // "Return true."
6927 return true;
6928 }
6930 // "Let end node equal node and let end offset equal offset."
6931 var endNode = node;
6932 var endOffset = offset;
6934 // "If end node has a child with index end offset, and that child is a
6935 // collapsed block prop, add one to end offset."
6936 if (endOffset < endNode.childNodes.length
6937 && isCollapsedBlockProp(endNode.childNodes[endOffset])) {
6938 endOffset++;
6939 }
6941 // "Repeat the following steps:"
6942 while (true) {
6943 // "If end offset is the length of end node, set end offset to one
6944 // plus the index of end node and then set end node to its parent."
6945 if (endOffset == getNodeLength(endNode)) {
6946 endOffset = 1 + getNodeIndex(endNode);
6947 endNode = endNode.parentNode;
6949 // "Otherwise, if end node has a an editable invisible child with
6950 // index end offset, remove it from end node."
6951 } else if (endOffset < endNode.childNodes.length
6952 && isEditable(endNode.childNodes[endOffset])
6953 && isInvisible(endNode.childNodes[endOffset])) {
6954 endNode.removeChild(endNode.childNodes[endOffset]);
6956 // "Otherwise, break from this loop."
6957 } else {
6958 break;
6959 }
6960 }
6962 // "If the child of end node with index end offset minus one is a
6963 // table, return true."
6964 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
6965 return true;
6966 }
6968 // "If the child of end node with index end offset is a table:"
6969 if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
6970 // "Call collapse(end node, end offset) on the context object's
6971 // Selection."
6972 getSelection().collapse(endNode, endOffset);
6973 getActiveRange().setStart(endNode, endOffset);
6975 // "Call extend(end node, end offset + 1) on the context object's
6976 // Selection."
6977 getSelection().extend(endNode, endOffset + 1);
6978 getActiveRange().setEnd(endNode, endOffset + 1);
6980 // "Return true."
6981 return true;
6982 }
6984 // "If offset is the length of node, and the child of end node with
6985 // index end offset is an hr or br:"
6986 if (offset == getNodeLength(node)
6987 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
6988 // "Call collapse(end node, end offset) on the context object's
6989 // Selection."
6990 getSelection().collapse(endNode, endOffset);
6991 getActiveRange().setStart(endNode, endOffset);
6993 // "Call extend(end node, end offset + 1) on the context object's
6994 // Selection."
6995 getSelection().extend(endNode, endOffset + 1);
6996 getActiveRange().setEnd(endNode, endOffset + 1);
6998 // "Delete the selection."
6999 deleteSelection();
7001 // "Call collapse(node, offset) on the Selection."
7002 getSelection().collapse(node, offset);
7003 getActiveRange().setStart(node, offset);
7004 getActiveRange().collapse(true);
7006 // "Return true."
7007 return true;
7008 }
7010 // "While end node has a child with index end offset:"
7011 while (endOffset < endNode.childNodes.length) {
7012 // "If end node's child with index end offset is editable and
7013 // invisible, remove it from end node."
7014 if (isEditable(endNode.childNodes[endOffset])
7015 && isInvisible(endNode.childNodes[endOffset])) {
7016 endNode.removeChild(endNode.childNodes[endOffset]);
7018 // "Otherwise, set end node to its child with index end offset and
7019 // set end offset to zero."
7020 } else {
7021 endNode = endNode.childNodes[endOffset];
7022 endOffset = 0;
7023 }
7024 }
7026 // "Call collapse(node, offset) on the context object's Selection."
7027 getSelection().collapse(node, offset);
7028 getActiveRange().setStart(node, offset);
7030 // "Call extend(end node, end offset) on the context object's
7031 // Selection."
7032 getSelection().extend(endNode, endOffset);
7033 getActiveRange().setEnd(endNode, endOffset);
7035 // "Delete the selection."
7036 deleteSelection();
7038 // "Return true."
7039 return true;
7040 }
7041 };
7043 //@}
7044 ///// The indent command /////
7045 //@{
7046 commands.indent = {
7047 preservesOverrides: true,
7048 action: function() {
7049 // "Let items be a list of all lis that are ancestor containers of the
7050 // active range's start and/or end node."
7051 //
7052 // Has to be in tree order, remember!
7053 var items = [];
7054 for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7055 if (isHtmlElement(node, "LI")) {
7056 items.unshift(node);
7057 }
7058 }
7059 for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7060 if (isHtmlElement(node, "LI")) {
7061 items.unshift(node);
7062 }
7063 }
7064 for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
7065 if (isHtmlElement(node, "LI")) {
7066 items.unshift(node);
7067 }
7068 }
7070 // "For each item in items, normalize sublists of item."
7071 for (var i = 0; i < items.length; i++) {
7072 normalizeSublists(items[i]);
7073 }
7075 // "Block-extend the active range, and let new range be the result."
7076 var newRange = blockExtend(getActiveRange());
7078 // "Let node list be a list of nodes, initially empty."
7079 var nodeList = [];
7081 // "For each node node contained in new range, if node is editable and
7082 // is an allowed child of "div" or "ol" and if the last member of node
7083 // list (if any) is not an ancestor of node, append node to node list."
7084 nodeList = getContainedNodes(newRange, function(node) {
7085 return isEditable(node)
7086 && (isAllowedChild(node, "div")
7087 || isAllowedChild(node, "ol"));
7088 });
7090 // "If the first visible member of node list is an li whose parent is
7091 // an ol or ul:"
7092 if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
7093 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) {
7094 // "Let sibling be node list's first visible member's
7095 // previousSibling."
7096 var sibling = nodeList.filter(isVisible)[0].previousSibling;
7098 // "While sibling is invisible, set sibling to its
7099 // previousSibling."
7100 while (isInvisible(sibling)) {
7101 sibling = sibling.previousSibling;
7102 }
7104 // "If sibling is an li, normalize sublists of sibling."
7105 if (isHtmlElement(sibling, "li")) {
7106 normalizeSublists(sibling);
7107 }
7108 }
7110 // "While node list is not empty:"
7111 while (nodeList.length) {
7112 // "Let sublist be a list of nodes, initially empty."
7113 var sublist = [];
7115 // "Remove the first member of node list and append it to sublist."
7116 sublist.push(nodeList.shift());
7118 // "While the first member of node list is the nextSibling of the
7119 // last member of sublist, remove the first member of node list and
7120 // append it to sublist."
7121 while (nodeList.length
7122 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
7123 sublist.push(nodeList.shift());
7124 }
7126 // "Indent sublist."
7127 indentNodes(sublist);
7128 }
7130 // "Return true."
7131 return true;
7132 }
7133 };
7135 //@}
7136 ///// The insertHorizontalRule command /////
7137 //@{
7138 commands.inserthorizontalrule = {
7139 preservesOverrides: true,
7140 action: function() {
7141 // "Let start node, start offset, end node, and end offset be the
7142 // active range's start and end nodes and offsets."
7143 var startNode = getActiveRange().startContainer;
7144 var startOffset = getActiveRange().startOffset;
7145 var endNode = getActiveRange().endContainer;
7146 var endOffset = getActiveRange().endOffset;
7148 // "While start offset is 0 and start node's parent is not null, set
7149 // start offset to start node's index, then set start node to its
7150 // parent."
7151 while (startOffset == 0
7152 && startNode.parentNode) {
7153 startOffset = getNodeIndex(startNode);
7154 startNode = startNode.parentNode;
7155 }
7157 // "While end offset is end node's length, and end node's parent is not
7158 // null, set end offset to one plus end node's index, then set end node
7159 // to its parent."
7160 while (endOffset == getNodeLength(endNode)
7161 && endNode.parentNode) {
7162 endOffset = 1 + getNodeIndex(endNode);
7163 endNode = endNode.parentNode;
7164 }
7166 // "Call collapse(start node, start offset) on the context object's
7167 // Selection."
7168 getSelection().collapse(startNode, startOffset);
7169 getActiveRange().setStart(startNode, startOffset);
7171 // "Call extend(end node, end offset) on the context object's
7172 // Selection."
7173 getSelection().extend(endNode, endOffset);
7174 getActiveRange().setEnd(endNode, endOffset);
7176 // "Delete the selection, with block merging false."
7177 deleteSelection({blockMerging: false});
7179 // "If the active range's start node is neither editable nor an editing
7180 // host, return true."
7181 if (!isEditable(getActiveRange().startContainer)
7182 && !isEditingHost(getActiveRange().startContainer)) {
7183 return true;
7184 }
7186 // "If the active range's start node is a Text node and its start
7187 // offset is zero, call collapse() on the context object's Selection,
7188 // with first argument the active range's start node's parent and
7189 // second argument the active range's start node's index."
7190 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7191 && getActiveRange().startOffset == 0) {
7192 var newNode = getActiveRange().startContainer.parentNode;
7193 var newOffset = getNodeIndex(getActiveRange().startContainer);
7194 getSelection().collapse(newNode, newOffset);
7195 getActiveRange().setStart(newNode, newOffset);
7196 getActiveRange().collapse(true);
7197 }
7199 // "If the active range's start node is a Text node and its start
7200 // offset is the length of its start node, call collapse() on the
7201 // context object's Selection, with first argument the active range's
7202 // start node's parent, and the second argument one plus the active
7203 // range's start node's index."
7204 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7205 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7206 var newNode = getActiveRange().startContainer.parentNode;
7207 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7208 getSelection().collapse(newNode, newOffset);
7209 getActiveRange().setStart(newNode, newOffset);
7210 getActiveRange().collapse(true);
7211 }
7213 // "Let hr be the result of calling createElement("hr") on the
7214 // context object."
7215 var hr = document.createElement("hr");
7217 // "Run insertNode(hr) on the active range."
7218 getActiveRange().insertNode(hr);
7220 // "Fix disallowed ancestors of hr."
7221 fixDisallowedAncestors(hr);
7223 // "Run collapse() on the context object's Selection, with first
7224 // argument hr's parent and the second argument equal to one plus hr's
7225 // index."
7226 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
7227 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
7228 getActiveRange().collapse(true);
7230 // "Return true."
7231 return true;
7232 }
7233 };
7235 //@}
7236 ///// The insertHTML command /////
7237 //@{
7238 commands.inserthtml = {
7239 preservesOverrides: true,
7240 action: function(value) {
7241 // "Delete the selection."
7242 deleteSelection();
7244 // "If the active range's start node is neither editable nor an editing
7245 // host, return true."
7246 if (!isEditable(getActiveRange().startContainer)
7247 && !isEditingHost(getActiveRange().startContainer)) {
7248 return true;
7249 }
7251 // "Let frag be the result of calling createContextualFragment(value)
7252 // on the active range."
7253 var frag = getActiveRange().createContextualFragment(value);
7255 // "Let last child be the lastChild of frag."
7256 var lastChild = frag.lastChild;
7258 // "If last child is null, return true."
7259 if (!lastChild) {
7260 return true;
7261 }
7263 // "Let descendants be all descendants of frag."
7264 var descendants = getDescendants(frag);
7266 // "If the active range's start node is a block node:"
7267 if (isBlockNode(getActiveRange().startContainer)) {
7268 // "Let collapsed block props be all editable collapsed block prop
7269 // children of the active range's start node that have index
7270 // greater than or equal to the active range's start offset."
7271 //
7272 // "For each node in collapsed block props, remove node from its
7273 // parent."
7274 [].filter.call(getActiveRange().startContainer.childNodes, function(node) {
7275 return isEditable(node)
7276 && isCollapsedBlockProp(node)
7277 && getNodeIndex(node) >= getActiveRange().startOffset;
7278 }).forEach(function(node) {
7279 node.parentNode.removeChild(node);
7280 });
7281 }
7283 // "Call insertNode(frag) on the active range."
7284 getActiveRange().insertNode(frag);
7286 // "If the active range's start node is a block node with no visible
7287 // children, call createElement("br") on the context object and append
7288 // the result as the last child of the active range's start node."
7289 if (isBlockNode(getActiveRange().startContainer)
7290 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
7291 getActiveRange().startContainer.appendChild(document.createElement("br"));
7292 }
7294 // "Call collapse() on the context object's Selection, with last
7295 // child's parent as the first argument and one plus its index as the
7296 // second."
7297 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7298 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7300 // "Fix disallowed ancestors of each member of descendants."
7301 for (var i = 0; i < descendants.length; i++) {
7302 fixDisallowedAncestors(descendants[i]);
7303 }
7305 // "Return true."
7306 return true;
7307 }
7308 };
7310 //@}
7311 ///// The insertImage command /////
7312 //@{
7313 commands.insertimage = {
7314 preservesOverrides: true,
7315 action: function(value) {
7316 // "If value is the empty string, return false."
7317 if (value === "") {
7318 return false;
7319 }
7321 // "Delete the selection, with strip wrappers false."
7322 deleteSelection({stripWrappers: false});
7324 // "Let range be the active range."
7325 var range = getActiveRange();
7327 // "If the active range's start node is neither editable nor an editing
7328 // host, return true."
7329 if (!isEditable(getActiveRange().startContainer)
7330 && !isEditingHost(getActiveRange().startContainer)) {
7331 return true;
7332 }
7334 // "If range's start node is a block node whose sole child is a br, and
7335 // its start offset is 0, remove its start node's child from it."
7336 if (isBlockNode(range.startContainer)
7337 && range.startContainer.childNodes.length == 1
7338 && isHtmlElement(range.startContainer.firstChild, "br")
7339 && range.startOffset == 0) {
7340 range.startContainer.removeChild(range.startContainer.firstChild);
7341 }
7343 // "Let img be the result of calling createElement("img") on the
7344 // context object."
7345 var img = document.createElement("img");
7347 // "Run setAttribute("src", value) on img."
7348 img.setAttribute("src", value);
7350 // "Run insertNode(img) on the range."
7351 range.insertNode(img);
7353 // "Run collapse() on the Selection, with first argument equal to the
7354 // parent of img and the second argument equal to one plus the index of
7355 // img."
7356 //
7357 // Not everyone actually supports collapse(), so we do it manually
7358 // instead. Also, we need to modify the actual range we're given as
7359 // well, for the sake of autoimplementation.html's range-filling-in.
7360 range.setStart(img.parentNode, 1 + getNodeIndex(img));
7361 range.setEnd(img.parentNode, 1 + getNodeIndex(img));
7362 getSelection().removeAllRanges();
7363 getSelection().addRange(range);
7365 // IE adds width and height attributes for some reason, so remove those
7366 // to actually do what the spec says.
7367 img.removeAttribute("width");
7368 img.removeAttribute("height");
7370 // "Return true."
7371 return true;
7372 }
7373 };
7375 //@}
7376 ///// The insertLineBreak command /////
7377 //@{
7378 commands.insertlinebreak = {
7379 preservesOverrides: true,
7380 action: function(value) {
7381 // "Delete the selection, with strip wrappers false."
7382 deleteSelection({stripWrappers: false});
7384 // "If the active range's start node is neither editable nor an editing
7385 // host, return true."
7386 if (!isEditable(getActiveRange().startContainer)
7387 && !isEditingHost(getActiveRange().startContainer)) {
7388 return true;
7389 }
7391 // "If the active range's start node is an Element, and "br" is not an
7392 // allowed child of it, return true."
7393 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
7394 && !isAllowedChild("br", getActiveRange().startContainer)) {
7395 return true;
7396 }
7398 // "If the active range's start node is not an Element, and "br" is not
7399 // an allowed child of the active range's start node's parent, return
7400 // true."
7401 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
7402 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
7403 return true;
7404 }
7406 // "If the active range's start node is a Text node and its start
7407 // offset is zero, call collapse() on the context object's Selection,
7408 // with first argument equal to the active range's start node's parent
7409 // and second argument equal to the active range's start node's index."
7410 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7411 && getActiveRange().startOffset == 0) {
7412 var newNode = getActiveRange().startContainer.parentNode;
7413 var newOffset = getNodeIndex(getActiveRange().startContainer);
7414 getSelection().collapse(newNode, newOffset);
7415 getActiveRange().setStart(newNode, newOffset);
7416 getActiveRange().setEnd(newNode, newOffset);
7417 }
7419 // "If the active range's start node is a Text node and its start
7420 // offset is the length of its start node, call collapse() on the
7421 // context object's Selection, with first argument equal to the active
7422 // range's start node's parent and second argument equal to one plus
7423 // the active range's start node's index."
7424 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7425 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7426 var newNode = getActiveRange().startContainer.parentNode;
7427 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7428 getSelection().collapse(newNode, newOffset);
7429 getActiveRange().setStart(newNode, newOffset);
7430 getActiveRange().setEnd(newNode, newOffset);
7431 }
7433 // "Let br be the result of calling createElement("br") on the context
7434 // object."
7435 var br = document.createElement("br");
7437 // "Call insertNode(br) on the active range."
7438 getActiveRange().insertNode(br);
7440 // "Call collapse() on the context object's Selection, with br's parent
7441 // as the first argument and one plus br's index as the second
7442 // argument."
7443 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7444 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7445 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7447 // "If br is a collapsed line break, call createElement("br") on the
7448 // context object and let extra br be the result, then call
7449 // insertNode(extra br) on the active range."
7450 if (isCollapsedLineBreak(br)) {
7451 getActiveRange().insertNode(document.createElement("br"));
7453 // Compensate for nonstandard implementations of insertNode
7454 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7455 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7456 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7457 }
7459 // "Return true."
7460 return true;
7461 }
7462 };
7464 //@}
7465 ///// The insertOrderedList command /////
7466 //@{
7467 commands.insertorderedlist = {
7468 preservesOverrides: true,
7469 // "Toggle lists with tag name "ol", then return true."
7470 action: function() { toggleLists("ol"); return true },
7471 // "True if the selection's list state is "mixed" or "mixed ol", false
7472 // otherwise."
7473 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
7474 // "True if the selection's list state is "ol", false otherwise."
7475 state: function() { return getSelectionListState() == "ol" },
7476 };
7478 //@}
7479 ///// The insertParagraph command /////
7480 //@{
7481 commands.insertparagraph = {
7482 preservesOverrides: true,
7483 action: function() {
7484 // "Delete the selection."
7485 deleteSelection();
7487 // "If the active range's start node is neither editable nor an editing
7488 // host, return true."
7489 if (!isEditable(getActiveRange().startContainer)
7490 && !isEditingHost(getActiveRange().startContainer)) {
7491 return true;
7492 }
7494 // "Let node and offset be the active range's start node and offset."
7495 var node = getActiveRange().startContainer;
7496 var offset = getActiveRange().startOffset;
7498 // "If node is a Text node, and offset is neither 0 nor the length of
7499 // node, call splitText(offset) on node."
7500 if (node.nodeType == Node.TEXT_NODE
7501 && offset != 0
7502 && offset != getNodeLength(node)) {
7503 node.splitText(offset);
7504 }
7506 // "If node is a Text node and offset is its length, set offset to one
7507 // plus the index of node, then set node to its parent."
7508 if (node.nodeType == Node.TEXT_NODE
7509 && offset == getNodeLength(node)) {
7510 offset = 1 + getNodeIndex(node);
7511 node = node.parentNode;
7512 }
7514 // "If node is a Text or Comment node, set offset to the index of node,
7515 // then set node to its parent."
7516 if (node.nodeType == Node.TEXT_NODE
7517 || node.nodeType == Node.COMMENT_NODE) {
7518 offset = getNodeIndex(node);
7519 node = node.parentNode;
7520 }
7522 // "Call collapse(node, offset) on the context object's Selection."
7523 getSelection().collapse(node, offset);
7524 getActiveRange().setStart(node, offset);
7525 getActiveRange().setEnd(node, offset);
7527 // "Let container equal node."
7528 var container = node;
7530 // "While container is not a single-line container, and container's
7531 // parent is editable and in the same editing host as node, set
7532 // container to its parent."
7533 while (!isSingleLineContainer(container)
7534 && isEditable(container.parentNode)
7535 && inSameEditingHost(node, container.parentNode)) {
7536 container = container.parentNode;
7537 }
7539 // "If container is an editable single-line container in the same
7540 // editing host as node, and its local name is "p" or "div":"
7541 if (isEditable(container)
7542 && isSingleLineContainer(container)
7543 && inSameEditingHost(node, container.parentNode)
7544 && (container.tagName == "P" || container.tagName == "DIV")) {
7545 // "Let outer container equal container."
7546 var outerContainer = container;
7548 // "While outer container is not a dd or dt or li, and outer
7549 // container's parent is editable, set outer container to its
7550 // parent."
7551 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
7552 && isEditable(outerContainer.parentNode)) {
7553 outerContainer = outerContainer.parentNode;
7554 }
7556 // "If outer container is a dd or dt or li, set container to outer
7557 // container."
7558 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
7559 container = outerContainer;
7560 }
7561 }
7563 // "If container is not editable or not in the same editing host as
7564 // node or is not a single-line container:"
7565 if (!isEditable(container)
7566 || !inSameEditingHost(container, node)
7567 || !isSingleLineContainer(container)) {
7568 // "Let tag be the default single-line container name."
7569 var tag = defaultSingleLineContainerName;
7571 // "Block-extend the active range, and let new range be the
7572 // result."
7573 var newRange = blockExtend(getActiveRange());
7575 // "Let node list be a list of nodes, initially empty."
7576 //
7577 // "Append to node list the first node in tree order that is
7578 // contained in new range and is an allowed child of "p", if any."
7579 var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
7580 .slice(0, 1);
7582 // "If node list is empty:"
7583 if (!nodeList.length) {
7584 // "If tag is not an allowed child of the active range's start
7585 // node, return true."
7586 if (!isAllowedChild(tag, getActiveRange().startContainer)) {
7587 return true;
7588 }
7590 // "Set container to the result of calling createElement(tag)
7591 // on the context object."
7592 container = document.createElement(tag);
7594 // "Call insertNode(container) on the active range."
7595 getActiveRange().insertNode(container);
7597 // "Call createElement("br") on the context object, and append
7598 // the result as the last child of container."
7599 container.appendChild(document.createElement("br"));
7601 // "Call collapse(container, 0) on the context object's
7602 // Selection."
7603 getSelection().collapse(container, 0);
7604 getActiveRange().setStart(container, 0);
7605 getActiveRange().setEnd(container, 0);
7607 // "Return true."
7608 return true;
7609 }
7611 // "While the nextSibling of the last member of node list is not
7612 // null and is an allowed child of "p", append it to node list."
7613 while (nodeList[nodeList.length - 1].nextSibling
7614 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
7615 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
7616 }
7618 // "Wrap node list, with sibling criteria returning false and new
7619 // parent instructions returning the result of calling
7620 // createElement(tag) on the context object. Set container to the
7621 // result."
7622 container = wrap(nodeList,
7623 function() { return false },
7624 function() { return document.createElement(tag) }
7625 );
7626 }
7628 // "If container's local name is "address", "listing", or "pre":"
7629 if (container.tagName == "ADDRESS"
7630 || container.tagName == "LISTING"
7631 || container.tagName == "PRE") {
7632 // "Let br be the result of calling createElement("br") on the
7633 // context object."
7634 var br = document.createElement("br");
7636 // "Call insertNode(br) on the active range."
7637 getActiveRange().insertNode(br);
7639 // "Call collapse(node, offset + 1) on the context object's
7640 // Selection."
7641 getSelection().collapse(node, offset + 1);
7642 getActiveRange().setStart(node, offset + 1);
7643 getActiveRange().setEnd(node, offset + 1);
7645 // "If br is the last descendant of container, let br be the result
7646 // of calling createElement("br") on the context object, then call
7647 // insertNode(br) on the active range."
7648 //
7649 // Work around browser bugs: some browsers select the
7650 // newly-inserted node, not per spec.
7651 if (!isDescendant(nextNode(br), container)) {
7652 getActiveRange().insertNode(document.createElement("br"));
7653 getSelection().collapse(node, offset + 1);
7654 getActiveRange().setEnd(node, offset + 1);
7655 }
7657 // "Return true."
7658 return true;
7659 }
7661 // "If container's local name is "li", "dt", or "dd"; and either it has
7662 // no children or it has a single child and that child is a br:"
7663 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
7664 && (!container.hasChildNodes()
7665 || (container.childNodes.length == 1
7666 && isHtmlElement(container.firstChild, "br")))) {
7667 // "Split the parent of the one-node list consisting of container."
7668 splitParent([container]);
7670 // "If container has no children, call createElement("br") on the
7671 // context object and append the result as the last child of
7672 // container."
7673 if (!container.hasChildNodes()) {
7674 container.appendChild(document.createElement("br"));
7675 }
7677 // "If container is a dd or dt, and it is not an allowed child of
7678 // any of its ancestors in the same editing host, set the tag name
7679 // of container to the default single-line container name and let
7680 // container be the result."
7681 if (isHtmlElement(container, ["dd", "dt"])
7682 && getAncestors(container).every(function(ancestor) {
7683 return !inSameEditingHost(container, ancestor)
7684 || !isAllowedChild(container, ancestor)
7685 })) {
7686 container = setTagName(container, defaultSingleLineContainerName);
7687 }
7689 // "Fix disallowed ancestors of container."
7690 fixDisallowedAncestors(container);
7692 // "Return true."
7693 return true;
7694 }
7696 // "Let new line range be a new range whose start is the same as
7697 // the active range's, and whose end is (container, length of
7698 // container)."
7699 var newLineRange = document.createRange();
7700 newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset);
7701 newLineRange.setEnd(container, getNodeLength(container));
7703 // "While new line range's start offset is zero and its start node is
7704 // not a prohibited paragraph child, set its start to (parent of start
7705 // node, index of start node)."
7706 while (newLineRange.startOffset == 0
7707 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7708 newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
7709 }
7711 // "While new line range's start offset is the length of its start node
7712 // and its start node is not a prohibited paragraph child, set its
7713 // start to (parent of start node, 1 + index of start node)."
7714 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
7715 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7716 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
7717 }
7719 // "Let end of line be true if new line range contains either nothing
7720 // or a single br, and false otherwise."
7721 var containedInNewLineRange = getContainedNodes(newLineRange);
7722 var endOfLine = !containedInNewLineRange.length
7723 || (containedInNewLineRange.length == 1
7724 && isHtmlElement(containedInNewLineRange[0], "br"));
7726 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
7727 // "h6", and end of line is true, let new container name be the default
7728 // single-line container name."
7729 var newContainerName;
7730 if (/^H[1-6]$/.test(container.tagName)
7731 && endOfLine) {
7732 newContainerName = defaultSingleLineContainerName;
7734 // "Otherwise, if the local name of container is "dt" and end of line
7735 // is true, let new container name be "dd"."
7736 } else if (container.tagName == "DT"
7737 && endOfLine) {
7738 newContainerName = "dd";
7740 // "Otherwise, if the local name of container is "dd" and end of line
7741 // is true, let new container name be "dt"."
7742 } else if (container.tagName == "DD"
7743 && endOfLine) {
7744 newContainerName = "dt";
7746 // "Otherwise, let new container name be the local name of container."
7747 } else {
7748 newContainerName = container.tagName.toLowerCase();
7749 }
7751 // "Let new container be the result of calling createElement(new
7752 // container name) on the context object."
7753 var newContainer = document.createElement(newContainerName);
7755 // "Copy all attributes of container to new container."
7756 for (var i = 0; i < container.attributes.length; i++) {
7757 newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value);
7758 }
7760 // "If new container has an id attribute, unset it."
7761 newContainer.removeAttribute("id");
7763 // "Insert new container into the parent of container immediately after
7764 // container."
7765 container.parentNode.insertBefore(newContainer, container.nextSibling);
7767 // "Let contained nodes be all nodes contained in new line range."
7768 var containedNodes = getAllContainedNodes(newLineRange);
7770 // "Let frag be the result of calling extractContents() on new line
7771 // range."
7772 var frag = newLineRange.extractContents();
7774 // "Unset the id attribute (if any) of each Element descendant of frag
7775 // that is not in contained nodes."
7776 var descendants = getDescendants(frag);
7777 for (var i = 0; i < descendants.length; i++) {
7778 if (descendants[i].nodeType == Node.ELEMENT_NODE
7779 && containedNodes.indexOf(descendants[i]) == -1) {
7780 descendants[i].removeAttribute("id");
7781 }
7782 }
7784 // "Call appendChild(frag) on new container."
7785 newContainer.appendChild(frag);
7787 // "While container's lastChild is a prohibited paragraph child, set
7788 // container to its lastChild."
7789 while (isProhibitedParagraphChild(container.lastChild)) {
7790 container = container.lastChild;
7791 }
7793 // "While new container's lastChild is a prohibited paragraph child,
7794 // set new container to its lastChild."
7795 while (isProhibitedParagraphChild(newContainer.lastChild)) {
7796 newContainer = newContainer.lastChild;
7797 }
7799 // "If container has no visible children, call createElement("br") on
7800 // the context object, and append the result as the last child of
7801 // container."
7802 if (![].some.call(container.childNodes, isVisible)) {
7803 container.appendChild(document.createElement("br"));
7804 }
7806 // "If new container has no visible children, call createElement("br")
7807 // on the context object, and append the result as the last child of
7808 // new container."
7809 if (![].some.call(newContainer.childNodes, isVisible)) {
7810 newContainer.appendChild(document.createElement("br"));
7811 }
7813 // "Call collapse(new container, 0) on the context object's Selection."
7814 getSelection().collapse(newContainer, 0);
7815 getActiveRange().setStart(newContainer, 0);
7816 getActiveRange().setEnd(newContainer, 0);
7818 // "Return true."
7819 return true;
7820 }
7821 };
7823 //@}
7824 ///// The insertText command /////
7825 //@{
7826 commands.inserttext = {
7827 action: function(value) {
7828 // "Delete the selection, with strip wrappers false."
7829 deleteSelection({stripWrappers: false});
7831 // "If the active range's start node is neither editable nor an editing
7832 // host, return true."
7833 if (!isEditable(getActiveRange().startContainer)
7834 && !isEditingHost(getActiveRange().startContainer)) {
7835 return true;
7836 }
7838 // "If value's length is greater than one:"
7839 if (value.length > 1) {
7840 // "For each element el in value, take the action for the
7841 // insertText command, with value equal to el."
7842 for (var i = 0; i < value.length; i++) {
7843 commands.inserttext.action(value[i]);
7844 }
7846 // "Return true."
7847 return true;
7848 }
7850 // "If value is the empty string, return true."
7851 if (value == "") {
7852 return true;
7853 }
7855 // "If value is a newline (U+00A0), take the action for the
7856 // insertParagraph command and return true."
7857 if (value == "\n") {
7858 commands.insertparagraph.action();
7859 return true;
7860 }
7862 // "Let node and offset be the active range's start node and offset."
7863 var node = getActiveRange().startContainer;
7864 var offset = getActiveRange().startOffset;
7866 // "If node has a child whose index is offset − 1, and that child is a
7867 // Text node, set node to that child, then set offset to node's
7868 // length."
7869 if (0 <= offset - 1
7870 && offset - 1 < node.childNodes.length
7871 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
7872 node = node.childNodes[offset - 1];
7873 offset = getNodeLength(node);
7874 }
7876 // "If node has a child whose index is offset, and that child is a Text
7877 // node, set node to that child, then set offset to zero."
7878 if (0 <= offset
7879 && offset < node.childNodes.length
7880 && node.childNodes[offset].nodeType == Node.TEXT_NODE) {
7881 node = node.childNodes[offset];
7882 offset = 0;
7883 }
7885 // "Record current overrides, and let overrides be the result."
7886 var overrides = recordCurrentOverrides();
7888 // "Call collapse(node, offset) on the context object's Selection."
7889 getSelection().collapse(node, offset);
7890 getActiveRange().setStart(node, offset);
7891 getActiveRange().setEnd(node, offset);
7893 // "Canonicalize whitespace at (node, offset)."
7894 canonicalizeWhitespace(node, offset);
7896 // "Let (node, offset) be the active range's start."
7897 node = getActiveRange().startContainer;
7898 offset = getActiveRange().startOffset;
7900 // "If node is a Text node:"
7901 if (node.nodeType == Node.TEXT_NODE) {
7902 // "Call insertData(offset, value) on node."
7903 node.insertData(offset, value);
7905 // "Call collapse(node, offset) on the context object's Selection."
7906 getSelection().collapse(node, offset);
7907 getActiveRange().setStart(node, offset);
7909 // "Call extend(node, offset + 1) on the context object's
7910 // Selection."
7911 //
7912 // Work around WebKit bug: the extend() can throw if the text we're
7913 // adding is trailing whitespace.
7914 try { getSelection().extend(node, offset + 1); } catch(e) {}
7915 getActiveRange().setEnd(node, offset + 1);
7917 // "Otherwise:"
7918 } else {
7919 // "If node has only one child, which is a collapsed line break,
7920 // remove its child from it."
7921 //
7922 // FIXME: IE incorrectly returns false here instead of true
7923 // sometimes?
7924 if (node.childNodes.length == 1
7925 && isCollapsedLineBreak(node.firstChild)) {
7926 node.removeChild(node.firstChild);
7927 }
7929 // "Let text be the result of calling createTextNode(value) on the
7930 // context object."
7931 var text = document.createTextNode(value);
7933 // "Call insertNode(text) on the active range."
7934 getActiveRange().insertNode(text);
7936 // "Call collapse(text, 0) on the context object's Selection."
7937 getSelection().collapse(text, 0);
7938 getActiveRange().setStart(text, 0);
7940 // "Call extend(text, 1) on the context object's Selection."
7941 getSelection().extend(text, 1);
7942 getActiveRange().setEnd(text, 1);
7943 }
7945 // "Restore states and values from overrides."
7946 restoreStatesAndValues(overrides);
7948 // "Canonicalize whitespace at the active range's start, with fix
7949 // collapsed space false."
7950 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
7952 // "Canonicalize whitespace at the active range's end, with fix
7953 // collapsed space false."
7954 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
7956 // "If value is a space character, autolink the active range's start."
7957 if (/^[ \t\n\f\r]$/.test(value)) {
7958 autolink(getActiveRange().startContainer, getActiveRange().startOffset);
7959 }
7961 // "Call collapseToEnd() on the context object's Selection."
7962 //
7963 // Work around WebKit bug: sometimes it blows up the selection and
7964 // throws, which we don't want.
7965 try { getSelection().collapseToEnd(); } catch(e) {}
7966 getActiveRange().collapse(false);
7968 // "Return true."
7969 return true;
7970 }
7971 };
7973 //@}
7974 ///// The insertUnorderedList command /////
7975 //@{
7976 commands.insertunorderedlist = {
7977 preservesOverrides: true,
7978 // "Toggle lists with tag name "ul", then return true."
7979 action: function() { toggleLists("ul"); return true },
7980 // "True if the selection's list state is "mixed" or "mixed ul", false
7981 // otherwise."
7982 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
7983 // "True if the selection's list state is "ul", false otherwise."
7984 state: function() { return getSelectionListState() == "ul" },
7985 };
7987 //@}
7988 ///// The justifyCenter command /////
7989 //@{
7990 commands.justifycenter = {
7991 preservesOverrides: true,
7992 // "Justify the selection with alignment "center", then return true."
7993 action: function() { justifySelection("center"); return true },
7994 indeterm: function() {
7995 // "Return false if the active range is null. Otherwise, block-extend
7996 // the active range. Return true if among visible editable nodes that
7997 // are contained in the result and have no children, at least one has
7998 // alignment value "center" and at least one does not. Otherwise return
7999 // false."
8000 if (!getActiveRange()) {
8001 return false;
8002 }
8003 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8004 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8005 });
8006 return nodes.some(function(node) { return getAlignmentValue(node) == "center" })
8007 && nodes.some(function(node) { return getAlignmentValue(node) != "center" });
8008 }, state: function() {
8009 // "Return false if the active range is null. Otherwise, block-extend
8010 // the active range. Return true if there is at least one visible
8011 // editable node that is contained in the result and has no children,
8012 // and all such nodes have alignment value "center". Otherwise return
8013 // false."
8014 if (!getActiveRange()) {
8015 return false;
8016 }
8017 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8018 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8019 });
8020 return nodes.length
8021 && nodes.every(function(node) { return getAlignmentValue(node) == "center" });
8022 }, value: function() {
8023 // "Return the empty string if the active range is null. Otherwise,
8024 // block-extend the active range, and return the alignment value of the
8025 // first visible editable node that is contained in the result and has
8026 // no children. If there is no such node, return "left"."
8027 if (!getActiveRange()) {
8028 return "";
8029 }
8030 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8031 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8032 });
8033 if (nodes.length) {
8034 return getAlignmentValue(nodes[0]);
8035 } else {
8036 return "left";
8037 }
8038 },
8039 };
8041 //@}
8042 ///// The justifyFull command /////
8043 //@{
8044 commands.justifyfull = {
8045 preservesOverrides: true,
8046 // "Justify the selection with alignment "justify", then return true."
8047 action: function() { justifySelection("justify"); return true },
8048 indeterm: function() {
8049 // "Return false if the active range is null. Otherwise, block-extend
8050 // the active range. Return true if among visible editable nodes that
8051 // are contained in the result and have no children, at least one has
8052 // alignment value "justify" and at least one does not. Otherwise
8053 // return false."
8054 if (!getActiveRange()) {
8055 return false;
8056 }
8057 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8058 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8059 });
8060 return nodes.some(function(node) { return getAlignmentValue(node) == "justify" })
8061 && nodes.some(function(node) { return getAlignmentValue(node) != "justify" });
8062 }, state: function() {
8063 // "Return false if the active range is null. Otherwise, block-extend
8064 // the active range. Return true if there is at least one visible
8065 // editable node that is contained in the result and has no children,
8066 // and all such nodes have alignment value "justify". Otherwise return
8067 // false."
8068 if (!getActiveRange()) {
8069 return false;
8070 }
8071 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8072 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8073 });
8074 return nodes.length
8075 && nodes.every(function(node) { return getAlignmentValue(node) == "justify" });
8076 }, value: function() {
8077 // "Return the empty string if the active range is null. Otherwise,
8078 // block-extend the active range, and return the alignment value of the
8079 // first visible editable node that is contained in the result and has
8080 // no children. If there is no such node, return "left"."
8081 if (!getActiveRange()) {
8082 return "";
8083 }
8084 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8085 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8086 });
8087 if (nodes.length) {
8088 return getAlignmentValue(nodes[0]);
8089 } else {
8090 return "left";
8091 }
8092 },
8093 };
8095 //@}
8096 ///// The justifyLeft command /////
8097 //@{
8098 commands.justifyleft = {
8099 preservesOverrides: true,
8100 // "Justify the selection with alignment "left", then return true."
8101 action: function() { justifySelection("left"); return true },
8102 indeterm: function() {
8103 // "Return false if the active range is null. Otherwise, block-extend
8104 // the active range. Return true if among visible editable nodes that
8105 // are contained in the result and have no children, at least one has
8106 // alignment value "left" and at least one does not. Otherwise return
8107 // false."
8108 if (!getActiveRange()) {
8109 return false;
8110 }
8111 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8112 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8113 });
8114 return nodes.some(function(node) { return getAlignmentValue(node) == "left" })
8115 && nodes.some(function(node) { return getAlignmentValue(node) != "left" });
8116 }, state: function() {
8117 // "Return false if the active range is null. Otherwise, block-extend
8118 // the active range. Return true if there is at least one visible
8119 // editable node that is contained in the result and has no children,
8120 // and all such nodes have alignment value "left". Otherwise return
8121 // false."
8122 if (!getActiveRange()) {
8123 return false;
8124 }
8125 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8126 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8127 });
8128 return nodes.length
8129 && nodes.every(function(node) { return getAlignmentValue(node) == "left" });
8130 }, value: function() {
8131 // "Return the empty string if the active range is null. Otherwise,
8132 // block-extend the active range, and return the alignment value of the
8133 // first visible editable node that is contained in the result and has
8134 // no children. If there is no such node, return "left"."
8135 if (!getActiveRange()) {
8136 return "";
8137 }
8138 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8139 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8140 });
8141 if (nodes.length) {
8142 return getAlignmentValue(nodes[0]);
8143 } else {
8144 return "left";
8145 }
8146 },
8147 };
8149 //@}
8150 ///// The justifyRight command /////
8151 //@{
8152 commands.justifyright = {
8153 preservesOverrides: true,
8154 // "Justify the selection with alignment "right", then return true."
8155 action: function() { justifySelection("right"); return true },
8156 indeterm: function() {
8157 // "Return false if the active range is null. Otherwise, block-extend
8158 // the active range. Return true if among visible editable nodes that
8159 // are contained in the result and have no children, at least one has
8160 // alignment value "right" and at least one does not. Otherwise return
8161 // false."
8162 if (!getActiveRange()) {
8163 return false;
8164 }
8165 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8166 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8167 });
8168 return nodes.some(function(node) { return getAlignmentValue(node) == "right" })
8169 && nodes.some(function(node) { return getAlignmentValue(node) != "right" });
8170 }, state: function() {
8171 // "Return false if the active range is null. Otherwise, block-extend
8172 // the active range. Return true if there is at least one visible
8173 // editable node that is contained in the result and has no children,
8174 // and all such nodes have alignment value "right". Otherwise return
8175 // false."
8176 if (!getActiveRange()) {
8177 return false;
8178 }
8179 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8180 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8181 });
8182 return nodes.length
8183 && nodes.every(function(node) { return getAlignmentValue(node) == "right" });
8184 }, value: function() {
8185 // "Return the empty string if the active range is null. Otherwise,
8186 // block-extend the active range, and return the alignment value of the
8187 // first visible editable node that is contained in the result and has
8188 // no children. If there is no such node, return "left"."
8189 if (!getActiveRange()) {
8190 return "";
8191 }
8192 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8193 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8194 });
8195 if (nodes.length) {
8196 return getAlignmentValue(nodes[0]);
8197 } else {
8198 return "left";
8199 }
8200 },
8201 };
8203 //@}
8204 ///// The outdent command /////
8205 //@{
8206 commands.outdent = {
8207 preservesOverrides: true,
8208 action: function() {
8209 // "Let items be a list of all lis that are ancestor containers of the
8210 // range's start and/or end node."
8211 //
8212 // It's annoying to get this in tree order using functional stuff
8213 // without doing getDescendants(document), which is slow, so I do it
8214 // imperatively.
8215 var items = [];
8216 (function(){
8217 for (
8218 var ancestorContainer = getActiveRange().endContainer;
8219 ancestorContainer != getActiveRange().commonAncestorContainer;
8220 ancestorContainer = ancestorContainer.parentNode
8221 ) {
8222 if (isHtmlElement(ancestorContainer, "li")) {
8223 items.unshift(ancestorContainer);
8224 }
8225 }
8226 for (
8227 var ancestorContainer = getActiveRange().startContainer;
8228 ancestorContainer;
8229 ancestorContainer = ancestorContainer.parentNode
8230 ) {
8231 if (isHtmlElement(ancestorContainer, "li")) {
8232 items.unshift(ancestorContainer);
8233 }
8234 }
8235 })();
8237 // "For each item in items, normalize sublists of item."
8238 items.forEach(normalizeSublists);
8240 // "Block-extend the active range, and let new range be the result."
8241 var newRange = blockExtend(getActiveRange());
8243 // "Let node list be a list of nodes, initially empty."
8244 //
8245 // "For each node node contained in new range, append node to node list
8246 // if the last member of node list (if any) is not an ancestor of node;
8247 // node is editable; and either node has no editable descendants, or is
8248 // an ol or ul, or is an li whose parent is an ol or ul."
8249 var nodeList = getContainedNodes(newRange, function(node) {
8250 return isEditable(node)
8251 && (!getDescendants(node).some(isEditable)
8252 || isHtmlElement(node, ["ol", "ul"])
8253 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
8254 });
8256 // "While node list is not empty:"
8257 while (nodeList.length) {
8258 // "While the first member of node list is an ol or ul or is not
8259 // the child of an ol or ul, outdent it and remove it from node
8260 // list."
8261 while (nodeList.length
8262 && (isHtmlElement(nodeList[0], ["OL", "UL"])
8263 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
8264 outdentNode(nodeList.shift());
8265 }
8267 // "If node list is empty, break from these substeps."
8268 if (!nodeList.length) {
8269 break;
8270 }
8272 // "Let sublist be a list of nodes, initially empty."
8273 var sublist = [];
8275 // "Remove the first member of node list and append it to sublist."
8276 sublist.push(nodeList.shift());
8278 // "While the first member of node list is the nextSibling of the
8279 // last member of sublist, and the first member of node list is not
8280 // an ol or ul, remove the first member of node list and append it
8281 // to sublist."
8282 while (nodeList.length
8283 && nodeList[0] == sublist[sublist.length - 1].nextSibling
8284 && !isHtmlElement(nodeList[0], ["OL", "UL"])) {
8285 sublist.push(nodeList.shift());
8286 }
8288 // "Record the values of sublist, and let values be the result."
8289 var values = recordValues(sublist);
8291 // "Split the parent of sublist, with new parent null."
8292 splitParent(sublist);
8294 // "Fix disallowed ancestors of each member of sublist."
8295 sublist.forEach(fixDisallowedAncestors);
8297 // "Restore the values from values."
8298 restoreValues(values);
8299 }
8301 // "Return true."
8302 return true;
8303 }
8304 };
8306 //@}
8308 //////////////////////////////////
8309 ///// Miscellaneous commands /////
8310 //////////////////////////////////
8312 ///// The defaultParagraphSeparator command /////
8313 //@{
8314 commands.defaultparagraphseparator = {
8315 action: function(value) {
8316 // "Let value be converted to ASCII lowercase. If value is then equal
8317 // to "p" or "div", set the context object's default single-line
8318 // container name to value and return true. Otherwise, return false."
8319 value = value.toLowerCase();
8320 if (value == "p" || value == "div") {
8321 defaultSingleLineContainerName = value;
8322 return true;
8323 }
8324 return false;
8325 }, value: function() {
8326 // "Return the context object's default single-line container name."
8327 return defaultSingleLineContainerName;
8328 },
8329 };
8331 //@}
8332 ///// The selectAll command /////
8333 //@{
8334 commands.selectall = {
8335 // Note, this ignores the whole globalRange/getActiveRange() thing and
8336 // works with actual selections. Not suitable for autoimplementation.html.
8337 action: function() {
8338 // "Let target be the body element of the context object."
8339 var target = document.body;
8341 // "If target is null, let target be the context object's
8342 // documentElement."
8343 if (!target) {
8344 target = document.documentElement;
8345 }
8347 // "If target is null, call getSelection() on the context object, and
8348 // call removeAllRanges() on the result."
8349 if (!target) {
8350 getSelection().removeAllRanges();
8352 // "Otherwise, call getSelection() on the context object, and call
8353 // selectAllChildren(target) on the result."
8354 } else {
8355 getSelection().selectAllChildren(target);
8356 }
8358 // "Return true."
8359 return true;
8360 }
8361 };
8363 //@}
8364 ///// The styleWithCSS command /////
8365 //@{
8366 commands.stylewithcss = {
8367 action: function(value) {
8368 // "If value is an ASCII case-insensitive match for the string
8369 // "false", set the CSS styling flag to false. Otherwise, set the
8370 // CSS styling flag to true. Either way, return true."
8371 cssStylingFlag = String(value).toLowerCase() != "false";
8372 return true;
8373 }, state: function() { return cssStylingFlag }
8374 };
8376 //@}
8377 ///// The useCSS command /////
8378 //@{
8379 commands.usecss = {
8380 action: function(value) {
8381 // "If value is an ASCII case-insensitive match for the string "false",
8382 // set the CSS styling flag to true. Otherwise, set the CSS styling
8383 // flag to false. Either way, return true."
8384 cssStylingFlag = String(value).toLowerCase() == "false";
8385 return true;
8386 }
8387 };
8388 //@}
8390 // Some final setup
8391 //@{
8392 (function() {
8393 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
8394 // temporary, which means I need an extra closure to not leak the temporaries
8395 // into the global namespace. >:(
8396 var commandNames = [];
8397 for (var command in commands) {
8398 commandNames.push(command);
8399 }
8400 commandNames.forEach(function(command) {
8401 // "If a command does not have a relevant CSS property specified, it
8402 // defaults to null."
8403 if (!("relevantCssProperty" in commands[command])) {
8404 commands[command].relevantCssProperty = null;
8405 }
8407 // "If a command has inline command activated values defined but nothing
8408 // else defines when it is indeterminate, it is indeterminate if among
8409 // formattable nodes effectively contained in the active range, there is at
8410 // least one whose effective command value is one of the given values and
8411 // at least one whose effective command value is not one of the given
8412 // values."
8413 if ("inlineCommandActivatedValues" in commands[command]
8414 && !("indeterm" in commands[command])) {
8415 commands[command].indeterm = function() {
8416 if (!getActiveRange()) {
8417 return false;
8418 }
8420 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
8421 .map(function(node) { return getEffectiveCommandValue(node, command) });
8423 var matchingValues = values.filter(function(value) {
8424 return commands[command].inlineCommandActivatedValues.indexOf(value) != -1;
8425 });
8427 return matchingValues.length >= 1
8428 && values.length - matchingValues.length >= 1;
8429 };
8430 }
8432 // "If a command has inline command activated values defined, its state is
8433 // true if either no formattable node is effectively contained in the
8434 // active range, and the active range's start node's effective command
8435 // value is one of the given values; or if there is at least one
8436 // formattable node effectively contained in the active range, and all of
8437 // them have an effective command value equal to one of the given values."
8438 if ("inlineCommandActivatedValues" in commands[command]) {
8439 commands[command].state = function() {
8440 if (!getActiveRange()) {
8441 return false;
8442 }
8444 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
8446 if (nodes.length == 0) {
8447 return commands[command].inlineCommandActivatedValues
8448 .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1;
8449 } else {
8450 return nodes.every(function(node) {
8451 return commands[command].inlineCommandActivatedValues
8452 .indexOf(getEffectiveCommandValue(node, command)) != -1;
8453 });
8454 }
8455 };
8456 }
8458 // "If a command is a standard inline value command, it is indeterminate if
8459 // among formattable nodes that are effectively contained in the active
8460 // range, there are two that have distinct effective command values. Its
8461 // value is the effective command value of the first formattable node that
8462 // is effectively contained in the active range; or if there is no such
8463 // node, the effective command value of the active range's start node; or
8464 // if that is null, the empty string."
8465 if ("standardInlineValueCommand" in commands[command]) {
8466 commands[command].indeterm = function() {
8467 if (!getActiveRange()) {
8468 return false;
8469 }
8471 var values = getAllEffectivelyContainedNodes(getActiveRange())
8472 .filter(isFormattableNode)
8473 .map(function(node) { return getEffectiveCommandValue(node, command) });
8474 for (var i = 1; i < values.length; i++) {
8475 if (values[i] != values[i - 1]) {
8476 return true;
8477 }
8478 }
8479 return false;
8480 };
8482 commands[command].value = function() {
8483 if (!getActiveRange()) {
8484 return "";
8485 }
8487 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
8489 if (typeof refNode == "undefined") {
8490 refNode = getActiveRange().startContainer;
8491 }
8493 var ret = getEffectiveCommandValue(refNode, command);
8494 if (ret === null) {
8495 return "";
8496 }
8497 return ret;
8498 };
8499 }
8501 // "If a command preserves overrides, then before taking its action, the
8502 // user agent must record current overrides. After taking the action, if
8503 // the active range is collapsed, it must restore states and values from
8504 // the recorded list."
8505 if ("preservesOverrides" in commands[command]) {
8506 var oldAction = commands[command].action;
8508 commands[command].action = function(value) {
8509 var overrides = recordCurrentOverrides();
8510 var ret = oldAction(value);
8511 if (getActiveRange().collapsed) {
8512 restoreStatesAndValues(overrides);
8513 }
8514 return ret;
8515 };
8516 }
8517 });
8518 })();
8519 //@}
8521 // vim: foldmarker=@{,@} foldmethod=marker