1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/imptests/editing/implementation.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,8521 @@ 1.4 +"use strict"; 1.5 + 1.6 +var htmlNamespace = "http://www.w3.org/1999/xhtml"; 1.7 + 1.8 +var cssStylingFlag = false; 1.9 + 1.10 +var defaultSingleLineContainerName = "p"; 1.11 + 1.12 +// This is bad :( 1.13 +var globalRange = null; 1.14 + 1.15 +// Commands are stored in a dictionary where we call their actions and such 1.16 +var commands = {}; 1.17 + 1.18 +/////////////////////////////////////////////////////////////////////////////// 1.19 +////////////////////////////// Utility functions ////////////////////////////// 1.20 +/////////////////////////////////////////////////////////////////////////////// 1.21 +//@{ 1.22 + 1.23 +function nextNode(node) { 1.24 + if (node.hasChildNodes()) { 1.25 + return node.firstChild; 1.26 + } 1.27 + return nextNodeDescendants(node); 1.28 +} 1.29 + 1.30 +function previousNode(node) { 1.31 + if (node.previousSibling) { 1.32 + node = node.previousSibling; 1.33 + while (node.hasChildNodes()) { 1.34 + node = node.lastChild; 1.35 + } 1.36 + return node; 1.37 + } 1.38 + if (node.parentNode 1.39 + && node.parentNode.nodeType == Node.ELEMENT_NODE) { 1.40 + return node.parentNode; 1.41 + } 1.42 + return null; 1.43 +} 1.44 + 1.45 +function nextNodeDescendants(node) { 1.46 + while (node && !node.nextSibling) { 1.47 + node = node.parentNode; 1.48 + } 1.49 + if (!node) { 1.50 + return null; 1.51 + } 1.52 + return node.nextSibling; 1.53 +} 1.54 + 1.55 +/** 1.56 + * Returns true if ancestor is an ancestor of descendant, false otherwise. 1.57 + */ 1.58 +function isAncestor(ancestor, descendant) { 1.59 + return ancestor 1.60 + && descendant 1.61 + && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); 1.62 +} 1.63 + 1.64 +/** 1.65 + * Returns true if ancestor is an ancestor of or equal to descendant, false 1.66 + * otherwise. 1.67 + */ 1.68 +function isAncestorContainer(ancestor, descendant) { 1.69 + return (ancestor || descendant) 1.70 + && (ancestor == descendant || isAncestor(ancestor, descendant)); 1.71 +} 1.72 + 1.73 +/** 1.74 + * Returns true if descendant is a descendant of ancestor, false otherwise. 1.75 + */ 1.76 +function isDescendant(descendant, ancestor) { 1.77 + return ancestor 1.78 + && descendant 1.79 + && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); 1.80 +} 1.81 + 1.82 +/** 1.83 + * Returns true if node1 is before node2 in tree order, false otherwise. 1.84 + */ 1.85 +function isBefore(node1, node2) { 1.86 + return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING); 1.87 +} 1.88 + 1.89 +/** 1.90 + * Returns true if node1 is after node2 in tree order, false otherwise. 1.91 + */ 1.92 +function isAfter(node1, node2) { 1.93 + return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING); 1.94 +} 1.95 + 1.96 +function getAncestors(node) { 1.97 + var ancestors = []; 1.98 + while (node.parentNode) { 1.99 + ancestors.unshift(node.parentNode); 1.100 + node = node.parentNode; 1.101 + } 1.102 + return ancestors; 1.103 +} 1.104 + 1.105 +function getInclusiveAncestors(node) { 1.106 + return getAncestors(node).concat(node); 1.107 +} 1.108 + 1.109 +function getDescendants(node) { 1.110 + var descendants = []; 1.111 + var stop = nextNodeDescendants(node); 1.112 + while ((node = nextNode(node)) 1.113 + && node != stop) { 1.114 + descendants.push(node); 1.115 + } 1.116 + return descendants; 1.117 +} 1.118 + 1.119 +function getInclusiveDescendants(node) { 1.120 + return [node].concat(getDescendants(node)); 1.121 +} 1.122 + 1.123 +function convertProperty(property) { 1.124 + // Special-case for now 1.125 + var map = { 1.126 + "fontFamily": "font-family", 1.127 + "fontSize": "font-size", 1.128 + "fontStyle": "font-style", 1.129 + "fontWeight": "font-weight", 1.130 + "textDecoration": "text-decoration", 1.131 + }; 1.132 + if (typeof map[property] != "undefined") { 1.133 + return map[property]; 1.134 + } 1.135 + 1.136 + return property; 1.137 +} 1.138 + 1.139 +// Return the <font size=X> value for the given CSS size, or undefined if there 1.140 +// is none. 1.141 +function cssSizeToLegacy(cssVal) { 1.142 + return { 1.143 + "x-small": 1, 1.144 + "small": 2, 1.145 + "medium": 3, 1.146 + "large": 4, 1.147 + "x-large": 5, 1.148 + "xx-large": 6, 1.149 + "xxx-large": 7 1.150 + }[cssVal]; 1.151 +} 1.152 + 1.153 +// Return the CSS size given a legacy size. 1.154 +function legacySizeToCss(legacyVal) { 1.155 + return { 1.156 + 1: "x-small", 1.157 + 2: "small", 1.158 + 3: "medium", 1.159 + 4: "large", 1.160 + 5: "x-large", 1.161 + 6: "xx-large", 1.162 + 7: "xxx-large", 1.163 + }[legacyVal]; 1.164 +} 1.165 + 1.166 +// Opera 11 puts HTML elements in the null namespace, it seems. 1.167 +function isHtmlNamespace(ns) { 1.168 + return ns === null 1.169 + || ns === htmlNamespace; 1.170 +} 1.171 + 1.172 +// "the directionality" from HTML. I don't bother caring about non-HTML 1.173 +// elements. 1.174 +// 1.175 +// "The directionality of an element is either 'ltr' or 'rtl', and is 1.176 +// determined as per the first appropriate set of steps from the following 1.177 +// list:" 1.178 +function getDirectionality(element) { 1.179 + // "If the element's dir attribute is in the ltr state 1.180 + // The directionality of the element is 'ltr'." 1.181 + if (element.dir == "ltr") { 1.182 + return "ltr"; 1.183 + } 1.184 + 1.185 + // "If the element's dir attribute is in the rtl state 1.186 + // The directionality of the element is 'rtl'." 1.187 + if (element.dir == "rtl") { 1.188 + return "rtl"; 1.189 + } 1.190 + 1.191 + // "If the element's dir attribute is in the auto state 1.192 + // "If the element is a bdi element and the dir attribute is not in a 1.193 + // defined state (i.e. it is not present or has an invalid value) 1.194 + // [lots of complicated stuff] 1.195 + // 1.196 + // Skip this, since no browser implements it anyway. 1.197 + 1.198 + // "If the element is a root element and the dir attribute is not in a 1.199 + // defined state (i.e. it is not present or has an invalid value) 1.200 + // The directionality of the element is 'ltr'." 1.201 + if (!isHtmlElement(element.parentNode)) { 1.202 + return "ltr"; 1.203 + } 1.204 + 1.205 + // "If the element has a parent element and the dir attribute is not in a 1.206 + // defined state (i.e. it is not present or has an invalid value) 1.207 + // The directionality of the element is the same as the element's 1.208 + // parent element's directionality." 1.209 + return getDirectionality(element.parentNode); 1.210 +} 1.211 + 1.212 +//@} 1.213 + 1.214 +/////////////////////////////////////////////////////////////////////////////// 1.215 +///////////////////////////// DOM Range functions ///////////////////////////// 1.216 +/////////////////////////////////////////////////////////////////////////////// 1.217 +//@{ 1.218 + 1.219 +function getNodeIndex(node) { 1.220 + var ret = 0; 1.221 + while (node.previousSibling) { 1.222 + ret++; 1.223 + node = node.previousSibling; 1.224 + } 1.225 + return ret; 1.226 +} 1.227 + 1.228 +// "The length of a Node node is the following, depending on node: 1.229 +// 1.230 +// ProcessingInstruction 1.231 +// DocumentType 1.232 +// Always 0. 1.233 +// Text 1.234 +// Comment 1.235 +// node's length. 1.236 +// Any other node 1.237 +// node's childNodes's length." 1.238 +function getNodeLength(node) { 1.239 + switch (node.nodeType) { 1.240 + case Node.PROCESSING_INSTRUCTION_NODE: 1.241 + case Node.DOCUMENT_TYPE_NODE: 1.242 + return 0; 1.243 + 1.244 + case Node.TEXT_NODE: 1.245 + case Node.COMMENT_NODE: 1.246 + return node.length; 1.247 + 1.248 + default: 1.249 + return node.childNodes.length; 1.250 + } 1.251 +} 1.252 + 1.253 +/** 1.254 + * The position of two boundary points relative to one another, as defined by 1.255 + * DOM Range. 1.256 + */ 1.257 +function getPosition(nodeA, offsetA, nodeB, offsetB) { 1.258 + // "If node A is the same as node B, return equal if offset A equals offset 1.259 + // B, before if offset A is less than offset B, and after if offset A is 1.260 + // greater than offset B." 1.261 + if (nodeA == nodeB) { 1.262 + if (offsetA == offsetB) { 1.263 + return "equal"; 1.264 + } 1.265 + if (offsetA < offsetB) { 1.266 + return "before"; 1.267 + } 1.268 + if (offsetA > offsetB) { 1.269 + return "after"; 1.270 + } 1.271 + } 1.272 + 1.273 + // "If node A is after node B in tree order, compute the position of (node 1.274 + // B, offset B) relative to (node A, offset A). If it is before, return 1.275 + // after. If it is after, return before." 1.276 + if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) { 1.277 + var pos = getPosition(nodeB, offsetB, nodeA, offsetA); 1.278 + if (pos == "before") { 1.279 + return "after"; 1.280 + } 1.281 + if (pos == "after") { 1.282 + return "before"; 1.283 + } 1.284 + } 1.285 + 1.286 + // "If node A is an ancestor of node B:" 1.287 + if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) { 1.288 + // "Let child equal node B." 1.289 + var child = nodeB; 1.290 + 1.291 + // "While child is not a child of node A, set child to its parent." 1.292 + while (child.parentNode != nodeA) { 1.293 + child = child.parentNode; 1.294 + } 1.295 + 1.296 + // "If the index of child is less than offset A, return after." 1.297 + if (getNodeIndex(child) < offsetA) { 1.298 + return "after"; 1.299 + } 1.300 + } 1.301 + 1.302 + // "Return before." 1.303 + return "before"; 1.304 +} 1.305 + 1.306 +/** 1.307 + * Returns the furthest ancestor of a Node as defined by DOM Range. 1.308 + */ 1.309 +function getFurthestAncestor(node) { 1.310 + var root = node; 1.311 + while (root.parentNode != null) { 1.312 + root = root.parentNode; 1.313 + } 1.314 + return root; 1.315 +} 1.316 + 1.317 +/** 1.318 + * "contained" as defined by DOM Range: "A Node node is contained in a range 1.319 + * range if node's furthest ancestor is the same as range's root, and (node, 0) 1.320 + * is after range's start, and (node, length of node) is before range's end." 1.321 + */ 1.322 +function isContained(node, range) { 1.323 + var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); 1.324 + var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); 1.325 + 1.326 + return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer) 1.327 + && pos1 == "after" 1.328 + && pos2 == "before"; 1.329 +} 1.330 + 1.331 +/** 1.332 + * Return all nodes contained in range that the provided function returns true 1.333 + * for, omitting any with an ancestor already being returned. 1.334 + */ 1.335 +function getContainedNodes(range, condition) { 1.336 + if (typeof condition == "undefined") { 1.337 + condition = function() { return true }; 1.338 + } 1.339 + var node = range.startContainer; 1.340 + if (node.hasChildNodes() 1.341 + && range.startOffset < node.childNodes.length) { 1.342 + // A child is contained 1.343 + node = node.childNodes[range.startOffset]; 1.344 + } else if (range.startOffset == getNodeLength(node)) { 1.345 + // No descendant can be contained 1.346 + node = nextNodeDescendants(node); 1.347 + } else { 1.348 + // No children; this node at least can't be contained 1.349 + node = nextNode(node); 1.350 + } 1.351 + 1.352 + var stop = range.endContainer; 1.353 + if (stop.hasChildNodes() 1.354 + && range.endOffset < stop.childNodes.length) { 1.355 + // The node after the last contained node is a child 1.356 + stop = stop.childNodes[range.endOffset]; 1.357 + } else { 1.358 + // This node and/or some of its children might be contained 1.359 + stop = nextNodeDescendants(stop); 1.360 + } 1.361 + 1.362 + var nodeList = []; 1.363 + while (isBefore(node, stop)) { 1.364 + if (isContained(node, range) 1.365 + && condition(node)) { 1.366 + nodeList.push(node); 1.367 + node = nextNodeDescendants(node); 1.368 + continue; 1.369 + } 1.370 + node = nextNode(node); 1.371 + } 1.372 + return nodeList; 1.373 +} 1.374 + 1.375 +/** 1.376 + * As above, but includes nodes with an ancestor that's already been returned. 1.377 + */ 1.378 +function getAllContainedNodes(range, condition) { 1.379 + if (typeof condition == "undefined") { 1.380 + condition = function() { return true }; 1.381 + } 1.382 + var node = range.startContainer; 1.383 + if (node.hasChildNodes() 1.384 + && range.startOffset < node.childNodes.length) { 1.385 + // A child is contained 1.386 + node = node.childNodes[range.startOffset]; 1.387 + } else if (range.startOffset == getNodeLength(node)) { 1.388 + // No descendant can be contained 1.389 + node = nextNodeDescendants(node); 1.390 + } else { 1.391 + // No children; this node at least can't be contained 1.392 + node = nextNode(node); 1.393 + } 1.394 + 1.395 + var stop = range.endContainer; 1.396 + if (stop.hasChildNodes() 1.397 + && range.endOffset < stop.childNodes.length) { 1.398 + // The node after the last contained node is a child 1.399 + stop = stop.childNodes[range.endOffset]; 1.400 + } else { 1.401 + // This node and/or some of its children might be contained 1.402 + stop = nextNodeDescendants(stop); 1.403 + } 1.404 + 1.405 + var nodeList = []; 1.406 + while (isBefore(node, stop)) { 1.407 + if (isContained(node, range) 1.408 + && condition(node)) { 1.409 + nodeList.push(node); 1.410 + } 1.411 + node = nextNode(node); 1.412 + } 1.413 + return nodeList; 1.414 +} 1.415 + 1.416 +// Returns either null, or something of the form rgb(x, y, z), or something of 1.417 +// the form rgb(x, y, z, w) with w != 0. 1.418 +function normalizeColor(color) { 1.419 + if (color.toLowerCase() == "currentcolor") { 1.420 + return null; 1.421 + } 1.422 + 1.423 + if (normalizeColor.resultCache === undefined) { 1.424 + normalizeColor.resultCache = {}; 1.425 + } 1.426 + 1.427 + if (normalizeColor.resultCache[color] !== undefined) { 1.428 + return normalizeColor.resultCache[color]; 1.429 + } 1.430 + 1.431 + var originalColor = color; 1.432 + 1.433 + var outerSpan = document.createElement("span"); 1.434 + document.body.appendChild(outerSpan); 1.435 + outerSpan.style.color = "black"; 1.436 + 1.437 + var innerSpan = document.createElement("span"); 1.438 + outerSpan.appendChild(innerSpan); 1.439 + innerSpan.style.color = color; 1.440 + color = getComputedStyle(innerSpan).color; 1.441 + 1.442 + if (color == "rgb(0, 0, 0)") { 1.443 + // Maybe it's really black, maybe it's invalid. 1.444 + outerSpan.color = "white"; 1.445 + color = getComputedStyle(innerSpan).color; 1.446 + if (color != "rgb(0, 0, 0)") { 1.447 + return normalizeColor.resultCache[originalColor] = null; 1.448 + } 1.449 + } 1.450 + 1.451 + document.body.removeChild(outerSpan); 1.452 + 1.453 + // I rely on the fact that browsers generally provide consistent syntax for 1.454 + // getComputedStyle(), although it's not standardized. There are only 1.455 + // three exceptions I found: 1.456 + if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { 1.457 + // IE10PP2 seems to do this sometimes. 1.458 + return normalizeColor.resultCache[originalColor] = 1.459 + color.replace("rgba", "rgb").replace(", 1)", ")"); 1.460 + } 1.461 + if (color == "transparent") { 1.462 + // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if 1.463 + // the specified value is "transparent". 1.464 + return normalizeColor.resultCache[originalColor] = 1.465 + "rgba(0, 0, 0, 0)"; 1.466 + } 1.467 + // Chrome 15 dev adds way too many significant figures. This isn't a full 1.468 + // fix, it just fixes one case that comes up in tests. 1.469 + color = color.replace(/, 0.496094\)$/, ", 0.5)"); 1.470 + return normalizeColor.resultCache[originalColor] = color; 1.471 +} 1.472 + 1.473 +// Returns either null, or something of the form #xxxxxx. 1.474 +function parseSimpleColor(color) { 1.475 + color = normalizeColor(color); 1.476 + var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); 1.477 + if (matches) { 1.478 + return "#" 1.479 + + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&") 1.480 + + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&") 1.481 + + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&"); 1.482 + } 1.483 + return null; 1.484 +} 1.485 + 1.486 +//@} 1.487 + 1.488 +////////////////////////////////////////////////////////////////////////////// 1.489 +/////////////////////////// Edit command functions /////////////////////////// 1.490 +////////////////////////////////////////////////////////////////////////////// 1.491 + 1.492 +///////////////////////////////////////////////// 1.493 +///// Methods of the HTMLDocument interface ///// 1.494 +///////////////////////////////////////////////// 1.495 +//@{ 1.496 + 1.497 +var executionStackDepth = 0; 1.498 + 1.499 +// Helper function for common behavior. 1.500 +function editCommandMethod(command, range, callback) { 1.501 + // Set up our global range magic, but only if we're the outermost function 1.502 + if (executionStackDepth == 0 && typeof range != "undefined") { 1.503 + globalRange = range; 1.504 + } else if (executionStackDepth == 0) { 1.505 + globalRange = null; 1.506 + globalRange = getActiveRange(); 1.507 + } 1.508 + 1.509 + executionStackDepth++; 1.510 + try { 1.511 + var ret = callback(); 1.512 + } catch(e) { 1.513 + executionStackDepth--; 1.514 + throw e; 1.515 + } 1.516 + executionStackDepth--; 1.517 + return ret; 1.518 +} 1.519 + 1.520 +function myExecCommand(command, showUi, value, range) { 1.521 + // "All of these methods must treat their command argument ASCII 1.522 + // case-insensitively." 1.523 + command = command.toLowerCase(); 1.524 + 1.525 + // "If only one argument was provided, let show UI be false." 1.526 + // 1.527 + // If range was passed, I can't actually detect how many args were passed 1.528 + // . . . 1.529 + if (arguments.length == 1 1.530 + || (arguments.length >=4 && typeof showUi == "undefined")) { 1.531 + showUi = false; 1.532 + } 1.533 + 1.534 + // "If only one or two arguments were provided, let value be the empty 1.535 + // string." 1.536 + if (arguments.length <= 2 1.537 + || (arguments.length >=4 && typeof value == "undefined")) { 1.538 + value = ""; 1.539 + } 1.540 + 1.541 + return editCommandMethod(command, range, (function(command, showUi, value) { return function() { 1.542 + // "If command is not supported or not enabled, return false." 1.543 + if (!(command in commands) || !myQueryCommandEnabled(command)) { 1.544 + return false; 1.545 + } 1.546 + 1.547 + // "Take the action for command, passing value to the instructions as an 1.548 + // argument." 1.549 + var ret = commands[command].action(value); 1.550 + 1.551 + // Check for bugs 1.552 + if (ret !== true && ret !== false) { 1.553 + throw "execCommand() didn't return true or false: " + ret; 1.554 + } 1.555 + 1.556 + // "If the previous step returned false, return false." 1.557 + if (ret === false) { 1.558 + return false; 1.559 + } 1.560 + 1.561 + // "Return true." 1.562 + return true; 1.563 + }})(command, showUi, value)); 1.564 +} 1.565 + 1.566 +function myQueryCommandEnabled(command, range) { 1.567 + // "All of these methods must treat their command argument ASCII 1.568 + // case-insensitively." 1.569 + command = command.toLowerCase(); 1.570 + 1.571 + return editCommandMethod(command, range, (function(command) { return function() { 1.572 + // "Return true if command is both supported and enabled, false 1.573 + // otherwise." 1.574 + if (!(command in commands)) { 1.575 + return false; 1.576 + } 1.577 + 1.578 + // "Among commands defined in this specification, those listed in 1.579 + // Miscellaneous commands are always enabled, except for the cut 1.580 + // command and the paste command. The other commands defined here are 1.581 + // enabled if the active range is not null, its start node is either 1.582 + // editable or an editing host, its end node is either editable or an 1.583 + // editing host, and there is some editing host that is an inclusive 1.584 + // ancestor of both its start node and its end node." 1.585 + return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss", 1.586 + "usecss"].indexOf(command) != -1 1.587 + || ( 1.588 + getActiveRange() !== null 1.589 + && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer)) 1.590 + && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer)) 1.591 + && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost)) 1.592 + ); 1.593 + }})(command)); 1.594 +} 1.595 + 1.596 +function myQueryCommandIndeterm(command, range) { 1.597 + // "All of these methods must treat their command argument ASCII 1.598 + // case-insensitively." 1.599 + command = command.toLowerCase(); 1.600 + 1.601 + return editCommandMethod(command, range, (function(command) { return function() { 1.602 + // "If command is not supported or has no indeterminacy, return false." 1.603 + if (!(command in commands) || !("indeterm" in commands[command])) { 1.604 + return false; 1.605 + } 1.606 + 1.607 + // "Return true if command is indeterminate, otherwise false." 1.608 + return commands[command].indeterm(); 1.609 + }})(command)); 1.610 +} 1.611 + 1.612 +function myQueryCommandState(command, range) { 1.613 + // "All of these methods must treat their command argument ASCII 1.614 + // case-insensitively." 1.615 + command = command.toLowerCase(); 1.616 + 1.617 + return editCommandMethod(command, range, (function(command) { return function() { 1.618 + // "If command is not supported or has no state, return false." 1.619 + if (!(command in commands) || !("state" in commands[command])) { 1.620 + return false; 1.621 + } 1.622 + 1.623 + // "If the state override for command is set, return it." 1.624 + if (typeof getStateOverride(command) != "undefined") { 1.625 + return getStateOverride(command); 1.626 + } 1.627 + 1.628 + // "Return true if command's state is true, otherwise false." 1.629 + return commands[command].state(); 1.630 + }})(command)); 1.631 +} 1.632 + 1.633 +// "When the queryCommandSupported(command) method on the HTMLDocument 1.634 +// interface is invoked, the user agent must return true if command is 1.635 +// supported, and false otherwise." 1.636 +function myQueryCommandSupported(command) { 1.637 + // "All of these methods must treat their command argument ASCII 1.638 + // case-insensitively." 1.639 + command = command.toLowerCase(); 1.640 + 1.641 + return command in commands; 1.642 +} 1.643 + 1.644 +function myQueryCommandValue(command, range) { 1.645 + // "All of these methods must treat their command argument ASCII 1.646 + // case-insensitively." 1.647 + command = command.toLowerCase(); 1.648 + 1.649 + return editCommandMethod(command, range, function() { 1.650 + // "If command is not supported or has no value, return the empty string." 1.651 + if (!(command in commands) || !("value" in commands[command])) { 1.652 + return ""; 1.653 + } 1.654 + 1.655 + // "If command is "fontSize" and its value override is set, convert the 1.656 + // value override to an integer number of pixels and return the legacy 1.657 + // font size for the result." 1.658 + if (command == "fontsize" 1.659 + && getValueOverride("fontsize") !== undefined) { 1.660 + return getLegacyFontSize(getValueOverride("fontsize")); 1.661 + } 1.662 + 1.663 + // "If the value override for command is set, return it." 1.664 + if (typeof getValueOverride(command) != "undefined") { 1.665 + return getValueOverride(command); 1.666 + } 1.667 + 1.668 + // "Return command's value." 1.669 + return commands[command].value(); 1.670 + }); 1.671 +} 1.672 +//@} 1.673 + 1.674 +////////////////////////////// 1.675 +///// Common definitions ///// 1.676 +////////////////////////////// 1.677 +//@{ 1.678 + 1.679 +// "An HTML element is an Element whose namespace is the HTML namespace." 1.680 +// 1.681 +// I allow an extra argument to more easily check whether something is a 1.682 +// particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays 1.683 +// too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul. 1.684 +function isHtmlElement(node, tags) { 1.685 + if (typeof tags == "string") { 1.686 + tags = [tags]; 1.687 + } 1.688 + if (typeof tags == "object") { 1.689 + tags = tags.map(function(tag) { return tag.toUpperCase() }); 1.690 + } 1.691 + return node 1.692 + && node.nodeType == Node.ELEMENT_NODE 1.693 + && isHtmlNamespace(node.namespaceURI) 1.694 + && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1); 1.695 +} 1.696 + 1.697 +// "A prohibited paragraph child name is "address", "article", "aside", 1.698 +// "blockquote", "caption", "center", "col", "colgroup", "dd", "details", 1.699 +// "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", 1.700 +// "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", 1.701 +// "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", 1.702 +// "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or 1.703 +// "xmp"." 1.704 +var prohibitedParagraphChildNames = ["address", "article", "aside", 1.705 + "blockquote", "caption", "center", "col", "colgroup", "dd", "details", 1.706 + "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", 1.707 + "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", 1.708 + "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", 1.709 + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", 1.710 + "xmp"]; 1.711 + 1.712 +// "A prohibited paragraph child is an HTML element whose local name is a 1.713 +// prohibited paragraph child name." 1.714 +function isProhibitedParagraphChild(node) { 1.715 + return isHtmlElement(node, prohibitedParagraphChildNames); 1.716 +} 1.717 + 1.718 +// "A block node is either an Element whose "display" property does not have 1.719 +// resolved value "inline" or "inline-block" or "inline-table" or "none", or a 1.720 +// Document, or a DocumentFragment." 1.721 +function isBlockNode(node) { 1.722 + return node 1.723 + && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1) 1.724 + || node.nodeType == Node.DOCUMENT_NODE 1.725 + || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE); 1.726 +} 1.727 + 1.728 +// "An inline node is a node that is not a block node." 1.729 +function isInlineNode(node) { 1.730 + return node && !isBlockNode(node); 1.731 +} 1.732 + 1.733 +// "An editing host is a node that is either an HTML element with a 1.734 +// contenteditable attribute set to the true state, or the HTML element child 1.735 +// of a Document whose designMode is enabled." 1.736 +function isEditingHost(node) { 1.737 + return node 1.738 + && isHtmlElement(node) 1.739 + && (node.contentEditable == "true" 1.740 + || (node.parentNode 1.741 + && node.parentNode.nodeType == Node.DOCUMENT_NODE 1.742 + && node.parentNode.designMode == "on")); 1.743 +} 1.744 + 1.745 +// "Something is editable if it is a node; it is not an editing host; it does 1.746 +// not have a contenteditable attribute set to the false state; its parent is 1.747 +// an editing host or editable; and either it is an HTML element, or it is an 1.748 +// svg or math element, or it is not an Element and its parent is an HTML 1.749 +// element." 1.750 +function isEditable(node) { 1.751 + return node 1.752 + && !isEditingHost(node) 1.753 + && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false") 1.754 + && (isEditingHost(node.parentNode) || isEditable(node.parentNode)) 1.755 + && (isHtmlElement(node) 1.756 + || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg") 1.757 + || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math") 1.758 + || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode))); 1.759 +} 1.760 + 1.761 +// Helper function, not defined in the spec 1.762 +function hasEditableDescendants(node) { 1.763 + for (var i = 0; i < node.childNodes.length; i++) { 1.764 + if (isEditable(node.childNodes[i]) 1.765 + || hasEditableDescendants(node.childNodes[i])) { 1.766 + return true; 1.767 + } 1.768 + } 1.769 + return false; 1.770 +} 1.771 + 1.772 +// "The editing host of node is null if node is neither editable nor an editing 1.773 +// host; node itself, if node is an editing host; or the nearest ancestor of 1.774 +// node that is an editing host, if node is editable." 1.775 +function getEditingHostOf(node) { 1.776 + if (isEditingHost(node)) { 1.777 + return node; 1.778 + } else if (isEditable(node)) { 1.779 + var ancestor = node.parentNode; 1.780 + while (!isEditingHost(ancestor)) { 1.781 + ancestor = ancestor.parentNode; 1.782 + } 1.783 + return ancestor; 1.784 + } else { 1.785 + return null; 1.786 + } 1.787 +} 1.788 + 1.789 +// "Two nodes are in the same editing host if the editing host of the first is 1.790 +// non-null and the same as the editing host of the second." 1.791 +function inSameEditingHost(node1, node2) { 1.792 + return getEditingHostOf(node1) 1.793 + && getEditingHostOf(node1) == getEditingHostOf(node2); 1.794 +} 1.795 + 1.796 +// "A collapsed line break is a br that begins a line box which has nothing 1.797 +// else in it, and therefore has zero height." 1.798 +function isCollapsedLineBreak(br) { 1.799 + if (!isHtmlElement(br, "br")) { 1.800 + return false; 1.801 + } 1.802 + 1.803 + // Add a zwsp after it and see if that changes the height of the nearest 1.804 + // non-inline parent. Note: this is not actually reliable, because the 1.805 + // parent might have a fixed height or something. 1.806 + var ref = br.parentNode; 1.807 + while (getComputedStyle(ref).display == "inline") { 1.808 + ref = ref.parentNode; 1.809 + } 1.810 + var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; 1.811 + ref.style.height = "auto"; 1.812 + ref.style.maxHeight = "none"; 1.813 + ref.style.minHeight = "0"; 1.814 + var space = document.createTextNode("\u200b"); 1.815 + var origHeight = ref.offsetHeight; 1.816 + if (origHeight == 0) { 1.817 + throw "isCollapsedLineBreak: original height is zero, bug?"; 1.818 + } 1.819 + br.parentNode.insertBefore(space, br.nextSibling); 1.820 + var finalHeight = ref.offsetHeight; 1.821 + space.parentNode.removeChild(space); 1.822 + if (refStyle === null) { 1.823 + // Without the setAttribute() line, removeAttribute() doesn't work in 1.824 + // Chrome 14 dev. I have no idea why. 1.825 + ref.setAttribute("style", ""); 1.826 + ref.removeAttribute("style"); 1.827 + } else { 1.828 + ref.setAttribute("style", refStyle); 1.829 + } 1.830 + 1.831 + // Allow some leeway in case the zwsp didn't create a whole new line, but 1.832 + // only made an existing line slightly higher. Firefox 6.0a2 shows this 1.833 + // behavior when the first line is bold. 1.834 + return origHeight < finalHeight - 5; 1.835 +} 1.836 + 1.837 +// "An extraneous line break is a br that has no visual effect, in that 1.838 +// removing it from the DOM would not change layout, except that a br that is 1.839 +// the sole child of an li is not extraneous." 1.840 +// 1.841 +// FIXME: This doesn't work in IE, since IE ignores display: none in 1.842 +// contenteditable. 1.843 +function isExtraneousLineBreak(br) { 1.844 + if (!isHtmlElement(br, "br")) { 1.845 + return false; 1.846 + } 1.847 + 1.848 + if (isHtmlElement(br.parentNode, "li") 1.849 + && br.parentNode.childNodes.length == 1) { 1.850 + return false; 1.851 + } 1.852 + 1.853 + // Make the line break disappear and see if that changes the block's 1.854 + // height. Yes, this is an absurd hack. We have to reset height etc. on 1.855 + // the reference node because otherwise its height won't change if it's not 1.856 + // auto. 1.857 + var ref = br.parentNode; 1.858 + while (getComputedStyle(ref).display == "inline") { 1.859 + ref = ref.parentNode; 1.860 + } 1.861 + var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; 1.862 + ref.style.height = "auto"; 1.863 + ref.style.maxHeight = "none"; 1.864 + ref.style.minHeight = "0"; 1.865 + var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null; 1.866 + var origHeight = ref.offsetHeight; 1.867 + if (origHeight == 0) { 1.868 + throw "isExtraneousLineBreak: original height is zero, bug?"; 1.869 + } 1.870 + br.setAttribute("style", "display:none"); 1.871 + var finalHeight = ref.offsetHeight; 1.872 + if (refStyle === null) { 1.873 + // Without the setAttribute() line, removeAttribute() doesn't work in 1.874 + // Chrome 14 dev. I have no idea why. 1.875 + ref.setAttribute("style", ""); 1.876 + ref.removeAttribute("style"); 1.877 + } else { 1.878 + ref.setAttribute("style", refStyle); 1.879 + } 1.880 + if (brStyle === null) { 1.881 + br.removeAttribute("style"); 1.882 + } else { 1.883 + br.setAttribute("style", brStyle); 1.884 + } 1.885 + 1.886 + return origHeight == finalHeight; 1.887 +} 1.888 + 1.889 +// "A whitespace node is either a Text node whose data is the empty string; or 1.890 +// a Text node whose data consists only of one or more tabs (0x0009), line 1.891 +// feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 1.892 +// parent is an Element whose resolved value for "white-space" is "normal" or 1.893 +// "nowrap"; or a Text node whose data consists only of one or more tabs 1.894 +// (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 1.895 +// parent is an Element whose resolved value for "white-space" is "pre-line"." 1.896 +function isWhitespaceNode(node) { 1.897 + return node 1.898 + && node.nodeType == Node.TEXT_NODE 1.899 + && (node.data == "" 1.900 + || ( 1.901 + /^[\t\n\r ]+$/.test(node.data) 1.902 + && node.parentNode 1.903 + && node.parentNode.nodeType == Node.ELEMENT_NODE 1.904 + && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1 1.905 + ) || ( 1.906 + /^[\t\r ]+$/.test(node.data) 1.907 + && node.parentNode 1.908 + && node.parentNode.nodeType == Node.ELEMENT_NODE 1.909 + && getComputedStyle(node.parentNode).whiteSpace == "pre-line" 1.910 + )); 1.911 +} 1.912 + 1.913 +// "node is a collapsed whitespace node if the following algorithm returns 1.914 +// true:" 1.915 +function isCollapsedWhitespaceNode(node) { 1.916 + // "If node is not a whitespace node, return false." 1.917 + if (!isWhitespaceNode(node)) { 1.918 + return false; 1.919 + } 1.920 + 1.921 + // "If node's data is the empty string, return true." 1.922 + if (node.data == "") { 1.923 + return true; 1.924 + } 1.925 + 1.926 + // "Let ancestor be node's parent." 1.927 + var ancestor = node.parentNode; 1.928 + 1.929 + // "If ancestor is null, return true." 1.930 + if (!ancestor) { 1.931 + return true; 1.932 + } 1.933 + 1.934 + // "If the "display" property of some ancestor of node has resolved value 1.935 + // "none", return true." 1.936 + if (getAncestors(node).some(function(ancestor) { 1.937 + return ancestor.nodeType == Node.ELEMENT_NODE 1.938 + && getComputedStyle(ancestor).display == "none"; 1.939 + })) { 1.940 + return true; 1.941 + } 1.942 + 1.943 + // "While ancestor is not a block node and its parent is not null, set 1.944 + // ancestor to its parent." 1.945 + while (!isBlockNode(ancestor) 1.946 + && ancestor.parentNode) { 1.947 + ancestor = ancestor.parentNode; 1.948 + } 1.949 + 1.950 + // "Let reference be node." 1.951 + var reference = node; 1.952 + 1.953 + // "While reference is a descendant of ancestor:" 1.954 + while (reference != ancestor) { 1.955 + // "Let reference be the node before it in tree order." 1.956 + reference = previousNode(reference); 1.957 + 1.958 + // "If reference is a block node or a br, return true." 1.959 + if (isBlockNode(reference) 1.960 + || isHtmlElement(reference, "br")) { 1.961 + return true; 1.962 + } 1.963 + 1.964 + // "If reference is a Text node that is not a whitespace node, or is an 1.965 + // img, break from this loop." 1.966 + if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) 1.967 + || isHtmlElement(reference, "img")) { 1.968 + break; 1.969 + } 1.970 + } 1.971 + 1.972 + // "Let reference be node." 1.973 + reference = node; 1.974 + 1.975 + // "While reference is a descendant of ancestor:" 1.976 + var stop = nextNodeDescendants(ancestor); 1.977 + while (reference != stop) { 1.978 + // "Let reference be the node after it in tree order, or null if there 1.979 + // is no such node." 1.980 + reference = nextNode(reference); 1.981 + 1.982 + // "If reference is a block node or a br, return true." 1.983 + if (isBlockNode(reference) 1.984 + || isHtmlElement(reference, "br")) { 1.985 + return true; 1.986 + } 1.987 + 1.988 + // "If reference is a Text node that is not a whitespace node, or is an 1.989 + // img, break from this loop." 1.990 + if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) 1.991 + || isHtmlElement(reference, "img")) { 1.992 + break; 1.993 + } 1.994 + } 1.995 + 1.996 + // "Return false." 1.997 + return false; 1.998 +} 1.999 + 1.1000 +// "Something is visible if it is a node that either is a block node, or a Text 1.1001 +// node that is not a collapsed whitespace node, or an img, or a br that is not 1.1002 +// an extraneous line break, or any node with a visible descendant; excluding 1.1003 +// any node with an ancestor container Element whose "display" property has 1.1004 +// resolved value "none"." 1.1005 +function isVisible(node) { 1.1006 + if (!node) { 1.1007 + return false; 1.1008 + } 1.1009 + 1.1010 + if (getAncestors(node).concat(node) 1.1011 + .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE }) 1.1012 + .some(function(node) { return getComputedStyle(node).display == "none" })) { 1.1013 + return false; 1.1014 + } 1.1015 + 1.1016 + if (isBlockNode(node) 1.1017 + || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) 1.1018 + || isHtmlElement(node, "img") 1.1019 + || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) { 1.1020 + return true; 1.1021 + } 1.1022 + 1.1023 + for (var i = 0; i < node.childNodes.length; i++) { 1.1024 + if (isVisible(node.childNodes[i])) { 1.1025 + return true; 1.1026 + } 1.1027 + } 1.1028 + 1.1029 + return false; 1.1030 +} 1.1031 + 1.1032 +// "Something is invisible if it is a node that is not visible." 1.1033 +function isInvisible(node) { 1.1034 + return node && !isVisible(node); 1.1035 +} 1.1036 + 1.1037 +// "A collapsed block prop is either a collapsed line break that is not an 1.1038 +// extraneous line break, or an Element that is an inline node and whose 1.1039 +// children are all either invisible or collapsed block props and that has at 1.1040 +// least one child that is a collapsed block prop." 1.1041 +function isCollapsedBlockProp(node) { 1.1042 + if (isCollapsedLineBreak(node) 1.1043 + && !isExtraneousLineBreak(node)) { 1.1044 + return true; 1.1045 + } 1.1046 + 1.1047 + if (!isInlineNode(node) 1.1048 + || node.nodeType != Node.ELEMENT_NODE) { 1.1049 + return false; 1.1050 + } 1.1051 + 1.1052 + var hasCollapsedBlockPropChild = false; 1.1053 + for (var i = 0; i < node.childNodes.length; i++) { 1.1054 + if (!isInvisible(node.childNodes[i]) 1.1055 + && !isCollapsedBlockProp(node.childNodes[i])) { 1.1056 + return false; 1.1057 + } 1.1058 + if (isCollapsedBlockProp(node.childNodes[i])) { 1.1059 + hasCollapsedBlockPropChild = true; 1.1060 + } 1.1061 + } 1.1062 + 1.1063 + return hasCollapsedBlockPropChild; 1.1064 +} 1.1065 + 1.1066 +// "The active range is the range of the selection given by calling 1.1067 +// getSelection() on the context object. (Thus the active range may be null.)" 1.1068 +// 1.1069 +// We cheat and return globalRange if that's defined. We also ensure that the 1.1070 +// active range meets the requirements that selection boundary points are 1.1071 +// supposed to meet, i.e., that the nodes are both Text or Element nodes that 1.1072 +// descend from a Document. 1.1073 +function getActiveRange() { 1.1074 + var ret; 1.1075 + if (globalRange) { 1.1076 + ret = globalRange; 1.1077 + } else if (getSelection().rangeCount) { 1.1078 + ret = getSelection().getRangeAt(0); 1.1079 + } else { 1.1080 + return null; 1.1081 + } 1.1082 + if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1 1.1083 + || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1 1.1084 + || !ret.startContainer.ownerDocument 1.1085 + || !ret.endContainer.ownerDocument 1.1086 + || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) 1.1087 + || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { 1.1088 + throw "Invalid active range; test bug?"; 1.1089 + } 1.1090 + return ret; 1.1091 +} 1.1092 + 1.1093 +// "For some commands, each HTMLDocument must have a boolean state override 1.1094 +// and/or a string value override. These do not change the command's state or 1.1095 +// value, but change the way some algorithms behave, as specified in those 1.1096 +// algorithms' definitions. Initially, both must be unset for every command. 1.1097 +// Whenever the number of ranges in the Selection changes to something 1.1098 +// different, and whenever a boundary point of the range at a given index in 1.1099 +// the Selection changes to something different, the state override and value 1.1100 +// override must be unset for every command." 1.1101 +// 1.1102 +// We implement this crudely by using setters and getters. To verify that the 1.1103 +// selection hasn't changed, we copy the active range and just check the 1.1104 +// endpoints match. This isn't really correct, but it's good enough for us. 1.1105 +// Unset state/value overrides are undefined. We put everything in a function 1.1106 +// so no one can access anything except via the provided functions, since 1.1107 +// otherwise callers might mistakenly use outdated overrides (if the selection 1.1108 +// has changed). 1.1109 +var getStateOverride, setStateOverride, unsetStateOverride, 1.1110 + getValueOverride, setValueOverride, unsetValueOverride; 1.1111 +(function() { 1.1112 + var stateOverrides = {}; 1.1113 + var valueOverrides = {}; 1.1114 + var storedRange = null; 1.1115 + 1.1116 + function resetOverrides() { 1.1117 + if (!storedRange 1.1118 + || storedRange.startContainer != getActiveRange().startContainer 1.1119 + || storedRange.endContainer != getActiveRange().endContainer 1.1120 + || storedRange.startOffset != getActiveRange().startOffset 1.1121 + || storedRange.endOffset != getActiveRange().endOffset) { 1.1122 + stateOverrides = {}; 1.1123 + valueOverrides = {}; 1.1124 + storedRange = getActiveRange().cloneRange(); 1.1125 + } 1.1126 + } 1.1127 + 1.1128 + getStateOverride = function(command) { 1.1129 + resetOverrides(); 1.1130 + return stateOverrides[command]; 1.1131 + }; 1.1132 + 1.1133 + setStateOverride = function(command, newState) { 1.1134 + resetOverrides(); 1.1135 + stateOverrides[command] = newState; 1.1136 + }; 1.1137 + 1.1138 + unsetStateOverride = function(command) { 1.1139 + resetOverrides(); 1.1140 + delete stateOverrides[command]; 1.1141 + } 1.1142 + 1.1143 + getValueOverride = function(command) { 1.1144 + resetOverrides(); 1.1145 + return valueOverrides[command]; 1.1146 + } 1.1147 + 1.1148 + // "The value override for the backColor command must be the same as the 1.1149 + // value override for the hiliteColor command, such that setting one sets 1.1150 + // the other to the same thing and unsetting one unsets the other." 1.1151 + setValueOverride = function(command, newValue) { 1.1152 + resetOverrides(); 1.1153 + valueOverrides[command] = newValue; 1.1154 + if (command == "backcolor") { 1.1155 + valueOverrides.hilitecolor = newValue; 1.1156 + } else if (command == "hilitecolor") { 1.1157 + valueOverrides.backcolor = newValue; 1.1158 + } 1.1159 + } 1.1160 + 1.1161 + unsetValueOverride = function(command) { 1.1162 + resetOverrides(); 1.1163 + delete valueOverrides[command]; 1.1164 + if (command == "backcolor") { 1.1165 + delete valueOverrides.hilitecolor; 1.1166 + } else if (command == "hilitecolor") { 1.1167 + delete valueOverrides.backcolor; 1.1168 + } 1.1169 + } 1.1170 +})(); 1.1171 + 1.1172 +//@} 1.1173 + 1.1174 +///////////////////////////// 1.1175 +///// Common algorithms ///// 1.1176 +///////////////////////////// 1.1177 + 1.1178 +///// Assorted common algorithms ///// 1.1179 +//@{ 1.1180 + 1.1181 +// Magic array of extra ranges whose endpoints we want to preserve. 1.1182 +var extraRanges = []; 1.1183 + 1.1184 +function movePreservingRanges(node, newParent, newIndex) { 1.1185 + // For convenience, I allow newIndex to be -1 to mean "insert at the end". 1.1186 + if (newIndex == -1) { 1.1187 + newIndex = newParent.childNodes.length; 1.1188 + } 1.1189 + 1.1190 + // "When the user agent is to move a Node to a new location, preserving 1.1191 + // ranges, it must remove the Node from its original parent (if any), then 1.1192 + // insert it in the new location. In doing so, however, it must ignore the 1.1193 + // regular range mutation rules, and instead follow these rules:" 1.1194 + 1.1195 + // "Let node be the moved Node, old parent and old index be the old parent 1.1196 + // (which may be null) and index, and new parent and new index be the new 1.1197 + // parent and index." 1.1198 + var oldParent = node.parentNode; 1.1199 + var oldIndex = getNodeIndex(node); 1.1200 + 1.1201 + // We preserve the global range object, the ranges in the selection, and 1.1202 + // any range that's in the extraRanges array. Any other ranges won't get 1.1203 + // updated, because we have no references to them. 1.1204 + var ranges = [globalRange].concat(extraRanges); 1.1205 + for (var i = 0; i < getSelection().rangeCount; i++) { 1.1206 + ranges.push(getSelection().getRangeAt(i)); 1.1207 + } 1.1208 + var boundaryPoints = []; 1.1209 + ranges.forEach(function(range) { 1.1210 + boundaryPoints.push([range.startContainer, range.startOffset]); 1.1211 + boundaryPoints.push([range.endContainer, range.endOffset]); 1.1212 + }); 1.1213 + 1.1214 + boundaryPoints.forEach(function(boundaryPoint) { 1.1215 + // "If a boundary point's node is the same as or a descendant of node, 1.1216 + // leave it unchanged, so it moves to the new location." 1.1217 + // 1.1218 + // No modifications necessary. 1.1219 + 1.1220 + // "If a boundary point's node is new parent and its offset is greater 1.1221 + // than new index, add one to its offset." 1.1222 + if (boundaryPoint[0] == newParent 1.1223 + && boundaryPoint[1] > newIndex) { 1.1224 + boundaryPoint[1]++; 1.1225 + } 1.1226 + 1.1227 + // "If a boundary point's node is old parent and its offset is old index or 1.1228 + // old index + 1, set its node to new parent and add new index − old index 1.1229 + // to its offset." 1.1230 + if (boundaryPoint[0] == oldParent 1.1231 + && (boundaryPoint[1] == oldIndex 1.1232 + || boundaryPoint[1] == oldIndex + 1)) { 1.1233 + boundaryPoint[0] = newParent; 1.1234 + boundaryPoint[1] += newIndex - oldIndex; 1.1235 + } 1.1236 + 1.1237 + // "If a boundary point's node is old parent and its offset is greater than 1.1238 + // old index + 1, subtract one from its offset." 1.1239 + if (boundaryPoint[0] == oldParent 1.1240 + && boundaryPoint[1] > oldIndex + 1) { 1.1241 + boundaryPoint[1]--; 1.1242 + } 1.1243 + }); 1.1244 + 1.1245 + // Now actually move it and preserve the ranges. 1.1246 + if (newParent.childNodes.length == newIndex) { 1.1247 + newParent.appendChild(node); 1.1248 + } else { 1.1249 + newParent.insertBefore(node, newParent.childNodes[newIndex]); 1.1250 + } 1.1251 + 1.1252 + globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); 1.1253 + globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); 1.1254 + 1.1255 + for (var i = 0; i < extraRanges.length; i++) { 1.1256 + extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]); 1.1257 + extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]); 1.1258 + } 1.1259 + 1.1260 + getSelection().removeAllRanges(); 1.1261 + for (var i = 1 + extraRanges.length; i < ranges.length; i++) { 1.1262 + var newRange = document.createRange(); 1.1263 + newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]); 1.1264 + newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]); 1.1265 + getSelection().addRange(newRange); 1.1266 + } 1.1267 +} 1.1268 + 1.1269 +function setTagName(element, newName) { 1.1270 + // "If element is an HTML element with local name equal to new name, return 1.1271 + // element." 1.1272 + if (isHtmlElement(element, newName.toUpperCase())) { 1.1273 + return element; 1.1274 + } 1.1275 + 1.1276 + // "If element's parent is null, return element." 1.1277 + if (!element.parentNode) { 1.1278 + return element; 1.1279 + } 1.1280 + 1.1281 + // "Let replacement element be the result of calling createElement(new 1.1282 + // name) on the ownerDocument of element." 1.1283 + var replacementElement = element.ownerDocument.createElement(newName); 1.1284 + 1.1285 + // "Insert replacement element into element's parent immediately before 1.1286 + // element." 1.1287 + element.parentNode.insertBefore(replacementElement, element); 1.1288 + 1.1289 + // "Copy all attributes of element to replacement element, in order." 1.1290 + for (var i = 0; i < element.attributes.length; i++) { 1.1291 + replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value); 1.1292 + } 1.1293 + 1.1294 + // "While element has children, append the first child of element as the 1.1295 + // last child of replacement element, preserving ranges." 1.1296 + while (element.childNodes.length) { 1.1297 + movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length); 1.1298 + } 1.1299 + 1.1300 + // "Remove element from its parent." 1.1301 + element.parentNode.removeChild(element); 1.1302 + 1.1303 + // "Return replacement element." 1.1304 + return replacementElement; 1.1305 +} 1.1306 + 1.1307 +function removeExtraneousLineBreaksBefore(node) { 1.1308 + // "Let ref be the previousSibling of node." 1.1309 + var ref = node.previousSibling; 1.1310 + 1.1311 + // "If ref is null, abort these steps." 1.1312 + if (!ref) { 1.1313 + return; 1.1314 + } 1.1315 + 1.1316 + // "While ref has children, set ref to its lastChild." 1.1317 + while (ref.hasChildNodes()) { 1.1318 + ref = ref.lastChild; 1.1319 + } 1.1320 + 1.1321 + // "While ref is invisible but not an extraneous line break, and ref does 1.1322 + // not equal node's parent, set ref to the node before it in tree order." 1.1323 + while (isInvisible(ref) 1.1324 + && !isExtraneousLineBreak(ref) 1.1325 + && ref != node.parentNode) { 1.1326 + ref = previousNode(ref); 1.1327 + } 1.1328 + 1.1329 + // "If ref is an editable extraneous line break, remove it from its 1.1330 + // parent." 1.1331 + if (isEditable(ref) 1.1332 + && isExtraneousLineBreak(ref)) { 1.1333 + ref.parentNode.removeChild(ref); 1.1334 + } 1.1335 +} 1.1336 + 1.1337 +function removeExtraneousLineBreaksAtTheEndOf(node) { 1.1338 + // "Let ref be node." 1.1339 + var ref = node; 1.1340 + 1.1341 + // "While ref has children, set ref to its lastChild." 1.1342 + while (ref.hasChildNodes()) { 1.1343 + ref = ref.lastChild; 1.1344 + } 1.1345 + 1.1346 + // "While ref is invisible but not an extraneous line break, and ref does 1.1347 + // not equal node, set ref to the node before it in tree order." 1.1348 + while (isInvisible(ref) 1.1349 + && !isExtraneousLineBreak(ref) 1.1350 + && ref != node) { 1.1351 + ref = previousNode(ref); 1.1352 + } 1.1353 + 1.1354 + // "If ref is an editable extraneous line break:" 1.1355 + if (isEditable(ref) 1.1356 + && isExtraneousLineBreak(ref)) { 1.1357 + // "While ref's parent is editable and invisible, set ref to its 1.1358 + // parent." 1.1359 + while (isEditable(ref.parentNode) 1.1360 + && isInvisible(ref.parentNode)) { 1.1361 + ref = ref.parentNode; 1.1362 + } 1.1363 + 1.1364 + // "Remove ref from its parent." 1.1365 + ref.parentNode.removeChild(ref); 1.1366 + } 1.1367 +} 1.1368 + 1.1369 +// "To remove extraneous line breaks from a node, first remove extraneous line 1.1370 +// breaks before it, then remove extraneous line breaks at the end of it." 1.1371 +function removeExtraneousLineBreaksFrom(node) { 1.1372 + removeExtraneousLineBreaksBefore(node); 1.1373 + removeExtraneousLineBreaksAtTheEndOf(node); 1.1374 +} 1.1375 + 1.1376 +//@} 1.1377 +///// Wrapping a list of nodes ///// 1.1378 +//@{ 1.1379 + 1.1380 +function wrap(nodeList, siblingCriteria, newParentInstructions) { 1.1381 + // "If not provided, sibling criteria returns false and new parent 1.1382 + // instructions returns null." 1.1383 + if (typeof siblingCriteria == "undefined") { 1.1384 + siblingCriteria = function() { return false }; 1.1385 + } 1.1386 + if (typeof newParentInstructions == "undefined") { 1.1387 + newParentInstructions = function() { return null }; 1.1388 + } 1.1389 + 1.1390 + // "If every member of node list is invisible, and none is a br, return 1.1391 + // null and abort these steps." 1.1392 + if (nodeList.every(isInvisible) 1.1393 + && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) { 1.1394 + return null; 1.1395 + } 1.1396 + 1.1397 + // "If node list's first member's parent is null, return null and abort 1.1398 + // these steps." 1.1399 + if (!nodeList[0].parentNode) { 1.1400 + return null; 1.1401 + } 1.1402 + 1.1403 + // "If node list's last member is an inline node that's not a br, and node 1.1404 + // list's last member's nextSibling is a br, append that br to node list." 1.1405 + if (isInlineNode(nodeList[nodeList.length - 1]) 1.1406 + && !isHtmlElement(nodeList[nodeList.length - 1], "br") 1.1407 + && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { 1.1408 + nodeList.push(nodeList[nodeList.length - 1].nextSibling); 1.1409 + } 1.1410 + 1.1411 + // "While node list's first member's previousSibling is invisible, prepend 1.1412 + // it to node list." 1.1413 + while (isInvisible(nodeList[0].previousSibling)) { 1.1414 + nodeList.unshift(nodeList[0].previousSibling); 1.1415 + } 1.1416 + 1.1417 + // "While node list's last member's nextSibling is invisible, append it to 1.1418 + // node list." 1.1419 + while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) { 1.1420 + nodeList.push(nodeList[nodeList.length - 1].nextSibling); 1.1421 + } 1.1422 + 1.1423 + // "If the previousSibling of the first member of node list is editable and 1.1424 + // running sibling criteria on it returns true, let new parent be the 1.1425 + // previousSibling of the first member of node list." 1.1426 + var newParent; 1.1427 + if (isEditable(nodeList[0].previousSibling) 1.1428 + && siblingCriteria(nodeList[0].previousSibling)) { 1.1429 + newParent = nodeList[0].previousSibling; 1.1430 + 1.1431 + // "Otherwise, if the nextSibling of the last member of node list is 1.1432 + // editable and running sibling criteria on it returns true, let new parent 1.1433 + // be the nextSibling of the last member of node list." 1.1434 + } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) 1.1435 + && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { 1.1436 + newParent = nodeList[nodeList.length - 1].nextSibling; 1.1437 + 1.1438 + // "Otherwise, run new parent instructions, and let new parent be the 1.1439 + // result." 1.1440 + } else { 1.1441 + newParent = newParentInstructions(); 1.1442 + } 1.1443 + 1.1444 + // "If new parent is null, abort these steps and return null." 1.1445 + if (!newParent) { 1.1446 + return null; 1.1447 + } 1.1448 + 1.1449 + // "If new parent's parent is null:" 1.1450 + if (!newParent.parentNode) { 1.1451 + // "Insert new parent into the parent of the first member of node list 1.1452 + // immediately before the first member of node list." 1.1453 + nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); 1.1454 + 1.1455 + // "If any range has a boundary point with node equal to the parent of 1.1456 + // new parent and offset equal to the index of new parent, add one to 1.1457 + // that boundary point's offset." 1.1458 + // 1.1459 + // Only try to fix the global range. 1.1460 + if (globalRange.startContainer == newParent.parentNode 1.1461 + && globalRange.startOffset == getNodeIndex(newParent)) { 1.1462 + globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1); 1.1463 + } 1.1464 + if (globalRange.endContainer == newParent.parentNode 1.1465 + && globalRange.endOffset == getNodeIndex(newParent)) { 1.1466 + globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1); 1.1467 + } 1.1468 + } 1.1469 + 1.1470 + // "Let original parent be the parent of the first member of node list." 1.1471 + var originalParent = nodeList[0].parentNode; 1.1472 + 1.1473 + // "If new parent is before the first member of node list in tree order:" 1.1474 + if (isBefore(newParent, nodeList[0])) { 1.1475 + // "If new parent is not an inline node, but the last visible child of 1.1476 + // new parent and the first visible member of node list are both inline 1.1477 + // nodes, and the last child of new parent is not a br, call 1.1478 + // createElement("br") on the ownerDocument of new parent and append 1.1479 + // the result as the last child of new parent." 1.1480 + if (!isInlineNode(newParent) 1.1481 + && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0]) 1.1482 + && isInlineNode(nodeList.filter(isVisible)[0]) 1.1483 + && !isHtmlElement(newParent.lastChild, "BR")) { 1.1484 + newParent.appendChild(newParent.ownerDocument.createElement("br")); 1.1485 + } 1.1486 + 1.1487 + // "For each node in node list, append node as the last child of new 1.1488 + // parent, preserving ranges." 1.1489 + for (var i = 0; i < nodeList.length; i++) { 1.1490 + movePreservingRanges(nodeList[i], newParent, -1); 1.1491 + } 1.1492 + 1.1493 + // "Otherwise:" 1.1494 + } else { 1.1495 + // "If new parent is not an inline node, but the first visible child of 1.1496 + // new parent and the last visible member of node list are both inline 1.1497 + // nodes, and the last member of node list is not a br, call 1.1498 + // createElement("br") on the ownerDocument of new parent and insert 1.1499 + // the result as the first child of new parent." 1.1500 + if (!isInlineNode(newParent) 1.1501 + && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0]) 1.1502 + && isInlineNode(nodeList.filter(isVisible).slice(-1)[0]) 1.1503 + && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) { 1.1504 + newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild); 1.1505 + } 1.1506 + 1.1507 + // "For each node in node list, in reverse order, insert node as the 1.1508 + // first child of new parent, preserving ranges." 1.1509 + for (var i = nodeList.length - 1; i >= 0; i--) { 1.1510 + movePreservingRanges(nodeList[i], newParent, 0); 1.1511 + } 1.1512 + } 1.1513 + 1.1514 + // "If original parent is editable and has no children, remove it from its 1.1515 + // parent." 1.1516 + if (isEditable(originalParent) && !originalParent.hasChildNodes()) { 1.1517 + originalParent.parentNode.removeChild(originalParent); 1.1518 + } 1.1519 + 1.1520 + // "If new parent's nextSibling is editable and running sibling criteria on 1.1521 + // it returns true:" 1.1522 + if (isEditable(newParent.nextSibling) 1.1523 + && siblingCriteria(newParent.nextSibling)) { 1.1524 + // "If new parent is not an inline node, but new parent's last child 1.1525 + // and new parent's nextSibling's first child are both inline nodes, 1.1526 + // and new parent's last child is not a br, call createElement("br") on 1.1527 + // the ownerDocument of new parent and append the result as the last 1.1528 + // child of new parent." 1.1529 + if (!isInlineNode(newParent) 1.1530 + && isInlineNode(newParent.lastChild) 1.1531 + && isInlineNode(newParent.nextSibling.firstChild) 1.1532 + && !isHtmlElement(newParent.lastChild, "BR")) { 1.1533 + newParent.appendChild(newParent.ownerDocument.createElement("br")); 1.1534 + } 1.1535 + 1.1536 + // "While new parent's nextSibling has children, append its first child 1.1537 + // as the last child of new parent, preserving ranges." 1.1538 + while (newParent.nextSibling.hasChildNodes()) { 1.1539 + movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1); 1.1540 + } 1.1541 + 1.1542 + // "Remove new parent's nextSibling from its parent." 1.1543 + newParent.parentNode.removeChild(newParent.nextSibling); 1.1544 + } 1.1545 + 1.1546 + // "Remove extraneous line breaks from new parent." 1.1547 + removeExtraneousLineBreaksFrom(newParent); 1.1548 + 1.1549 + // "Return new parent." 1.1550 + return newParent; 1.1551 +} 1.1552 + 1.1553 + 1.1554 +//@} 1.1555 +///// Allowed children ///// 1.1556 +//@{ 1.1557 + 1.1558 +// "A name of an element with inline contents is "a", "abbr", "b", "bdi", 1.1559 +// "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", 1.1560 +// "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", 1.1561 +// "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", 1.1562 +// "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." 1.1563 +var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo", 1.1564 + "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", 1.1565 + "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", 1.1566 + "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", 1.1567 + "xmp", "big", "blink", "font", "marquee", "nobr", "tt"]; 1.1568 + 1.1569 +// "An element with inline contents is an HTML element whose local name is a 1.1570 +// name of an element with inline contents." 1.1571 +function isElementWithInlineContents(node) { 1.1572 + return isHtmlElement(node, namesOfElementsWithInlineContents); 1.1573 +} 1.1574 + 1.1575 +function isAllowedChild(child, parent_) { 1.1576 + // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or 1.1577 + // an HTML element with local name equal to one of those, and child is a 1.1578 + // Text node whose data does not consist solely of space characters, return 1.1579 + // false." 1.1580 + if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1 1.1581 + || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"])) 1.1582 + && typeof child == "object" 1.1583 + && child.nodeType == Node.TEXT_NODE 1.1584 + && !/^[ \t\n\f\r]*$/.test(child.data)) { 1.1585 + return false; 1.1586 + } 1.1587 + 1.1588 + // "If parent is "script", "style", "plaintext", or "xmp", or an HTML 1.1589 + // element with local name equal to one of those, and child is not a Text 1.1590 + // node, return false." 1.1591 + if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1 1.1592 + || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"])) 1.1593 + && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) { 1.1594 + return false; 1.1595 + } 1.1596 + 1.1597 + // "If child is a Document, DocumentFragment, or DocumentType, return 1.1598 + // false." 1.1599 + if (typeof child == "object" 1.1600 + && (child.nodeType == Node.DOCUMENT_NODE 1.1601 + || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE 1.1602 + || child.nodeType == Node.DOCUMENT_TYPE_NODE)) { 1.1603 + return false; 1.1604 + } 1.1605 + 1.1606 + // "If child is an HTML element, set child to the local name of child." 1.1607 + if (isHtmlElement(child)) { 1.1608 + child = child.tagName.toLowerCase(); 1.1609 + } 1.1610 + 1.1611 + // "If child is not a string, return true." 1.1612 + if (typeof child != "string") { 1.1613 + return true; 1.1614 + } 1.1615 + 1.1616 + // "If parent is an HTML element:" 1.1617 + if (isHtmlElement(parent_)) { 1.1618 + // "If child is "a", and parent or some ancestor of parent is an a, 1.1619 + // return false." 1.1620 + // 1.1621 + // "If child is a prohibited paragraph child name and parent or some 1.1622 + // ancestor of parent is an element with inline contents, return 1.1623 + // false." 1.1624 + // 1.1625 + // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or 1.1626 + // some ancestor of parent is an HTML element with local name "h1", 1.1627 + // "h2", "h3", "h4", "h5", or "h6", return false." 1.1628 + var ancestor = parent_; 1.1629 + while (ancestor) { 1.1630 + if (child == "a" && isHtmlElement(ancestor, "a")) { 1.1631 + return false; 1.1632 + } 1.1633 + if (prohibitedParagraphChildNames.indexOf(child) != -1 1.1634 + && isElementWithInlineContents(ancestor)) { 1.1635 + return false; 1.1636 + } 1.1637 + if (/^h[1-6]$/.test(child) 1.1638 + && isHtmlElement(ancestor) 1.1639 + && /^H[1-6]$/.test(ancestor.tagName)) { 1.1640 + return false; 1.1641 + } 1.1642 + ancestor = ancestor.parentNode; 1.1643 + } 1.1644 + 1.1645 + // "Let parent be the local name of parent." 1.1646 + parent_ = parent_.tagName.toLowerCase(); 1.1647 + } 1.1648 + 1.1649 + // "If parent is an Element or DocumentFragment, return true." 1.1650 + if (typeof parent_ == "object" 1.1651 + && (parent_.nodeType == Node.ELEMENT_NODE 1.1652 + || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { 1.1653 + return true; 1.1654 + } 1.1655 + 1.1656 + // "If parent is not a string, return false." 1.1657 + if (typeof parent_ != "string") { 1.1658 + return false; 1.1659 + } 1.1660 + 1.1661 + // "If parent is on the left-hand side of an entry on the following list, 1.1662 + // then return true if child is listed on the right-hand side of that 1.1663 + // entry, and false otherwise." 1.1664 + switch (parent_) { 1.1665 + case "colgroup": 1.1666 + return child == "col"; 1.1667 + case "table": 1.1668 + return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1; 1.1669 + case "tbody": 1.1670 + case "thead": 1.1671 + case "tfoot": 1.1672 + return ["td", "th", "tr"].indexOf(child) != -1; 1.1673 + case "tr": 1.1674 + return ["td", "th"].indexOf(child) != -1; 1.1675 + case "dl": 1.1676 + return ["dt", "dd"].indexOf(child) != -1; 1.1677 + case "dir": 1.1678 + case "ol": 1.1679 + case "ul": 1.1680 + return ["dir", "li", "ol", "ul"].indexOf(child) != -1; 1.1681 + case "hgroup": 1.1682 + return /^h[1-6]$/.test(child); 1.1683 + } 1.1684 + 1.1685 + // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", 1.1686 + // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return 1.1687 + // false." 1.1688 + if (["body", "caption", "col", "colgroup", "frame", "frameset", "head", 1.1689 + "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) { 1.1690 + return false; 1.1691 + } 1.1692 + 1.1693 + // "If child is "dd" or "dt" and parent is not "dl", return false." 1.1694 + if (["dd", "dt"].indexOf(child) != -1 1.1695 + && parent_ != "dl") { 1.1696 + return false; 1.1697 + } 1.1698 + 1.1699 + // "If child is "li" and parent is not "ol" or "ul", return false." 1.1700 + if (child == "li" 1.1701 + && parent_ != "ol" 1.1702 + && parent_ != "ul") { 1.1703 + return false; 1.1704 + } 1.1705 + 1.1706 + // "If parent is on the left-hand side of an entry on the following list 1.1707 + // and child is listed on the right-hand side of that entry, return false." 1.1708 + var table = [ 1.1709 + [["a"], ["a"]], 1.1710 + [["dd", "dt"], ["dd", "dt"]], 1.1711 + [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]], 1.1712 + [["li"], ["li"]], 1.1713 + [["nobr"], ["nobr"]], 1.1714 + [namesOfElementsWithInlineContents, prohibitedParagraphChildNames], 1.1715 + [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]], 1.1716 + ]; 1.1717 + for (var i = 0; i < table.length; i++) { 1.1718 + if (table[i][0].indexOf(parent_) != -1 1.1719 + && table[i][1].indexOf(child) != -1) { 1.1720 + return false; 1.1721 + } 1.1722 + } 1.1723 + 1.1724 + // "Return true." 1.1725 + return true; 1.1726 +} 1.1727 + 1.1728 + 1.1729 +//@} 1.1730 + 1.1731 +////////////////////////////////////// 1.1732 +///// Inline formatting commands ///// 1.1733 +////////////////////////////////////// 1.1734 + 1.1735 +///// Inline formatting command definitions ///// 1.1736 +//@{ 1.1737 + 1.1738 +// "A node node is effectively contained in a range range if range is not 1.1739 +// collapsed, and at least one of the following holds:" 1.1740 +function isEffectivelyContained(node, range) { 1.1741 + if (range.collapsed) { 1.1742 + return false; 1.1743 + } 1.1744 + 1.1745 + // "node is contained in range." 1.1746 + if (isContained(node, range)) { 1.1747 + return true; 1.1748 + } 1.1749 + 1.1750 + // "node is range's start node, it is a Text node, and its length is 1.1751 + // different from range's start offset." 1.1752 + if (node == range.startContainer 1.1753 + && node.nodeType == Node.TEXT_NODE 1.1754 + && getNodeLength(node) != range.startOffset) { 1.1755 + return true; 1.1756 + } 1.1757 + 1.1758 + // "node is range's end node, it is a Text node, and range's end offset is 1.1759 + // not 0." 1.1760 + if (node == range.endContainer 1.1761 + && node.nodeType == Node.TEXT_NODE 1.1762 + && range.endOffset != 0) { 1.1763 + return true; 1.1764 + } 1.1765 + 1.1766 + // "node has at least one child; and all its children are effectively 1.1767 + // contained in range; and either range's start node is not a descendant of 1.1768 + // node or is not a Text node or range's start offset is zero; and either 1.1769 + // range's end node is not a descendant of node or is not a Text node or 1.1770 + // range's end offset is its end node's length." 1.1771 + if (node.hasChildNodes() 1.1772 + && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) }) 1.1773 + && (!isDescendant(range.startContainer, node) 1.1774 + || range.startContainer.nodeType != Node.TEXT_NODE 1.1775 + || range.startOffset == 0) 1.1776 + && (!isDescendant(range.endContainer, node) 1.1777 + || range.endContainer.nodeType != Node.TEXT_NODE 1.1778 + || range.endOffset == getNodeLength(range.endContainer))) { 1.1779 + return true; 1.1780 + } 1.1781 + 1.1782 + return false; 1.1783 +} 1.1784 + 1.1785 +// Like get(All)ContainedNodes(), but for effectively contained nodes. 1.1786 +function getEffectivelyContainedNodes(range, condition) { 1.1787 + if (typeof condition == "undefined") { 1.1788 + condition = function() { return true }; 1.1789 + } 1.1790 + var node = range.startContainer; 1.1791 + while (isEffectivelyContained(node.parentNode, range)) { 1.1792 + node = node.parentNode; 1.1793 + } 1.1794 + 1.1795 + var stop = nextNodeDescendants(range.endContainer); 1.1796 + 1.1797 + var nodeList = []; 1.1798 + while (isBefore(node, stop)) { 1.1799 + if (isEffectivelyContained(node, range) 1.1800 + && condition(node)) { 1.1801 + nodeList.push(node); 1.1802 + node = nextNodeDescendants(node); 1.1803 + continue; 1.1804 + } 1.1805 + node = nextNode(node); 1.1806 + } 1.1807 + return nodeList; 1.1808 +} 1.1809 + 1.1810 +function getAllEffectivelyContainedNodes(range, condition) { 1.1811 + if (typeof condition == "undefined") { 1.1812 + condition = function() { return true }; 1.1813 + } 1.1814 + var node = range.startContainer; 1.1815 + while (isEffectivelyContained(node.parentNode, range)) { 1.1816 + node = node.parentNode; 1.1817 + } 1.1818 + 1.1819 + var stop = nextNodeDescendants(range.endContainer); 1.1820 + 1.1821 + var nodeList = []; 1.1822 + while (isBefore(node, stop)) { 1.1823 + if (isEffectivelyContained(node, range) 1.1824 + && condition(node)) { 1.1825 + nodeList.push(node); 1.1826 + } 1.1827 + node = nextNode(node); 1.1828 + } 1.1829 + return nodeList; 1.1830 +} 1.1831 + 1.1832 +// "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element 1.1833 +// with no attributes except possibly style; or a font element with no 1.1834 +// attributes except possibly style, color, face, and/or size; or an a element 1.1835 +// with no attributes except possibly style and/or href." 1.1836 +function isModifiableElement(node) { 1.1837 + if (!isHtmlElement(node)) { 1.1838 + return false; 1.1839 + } 1.1840 + 1.1841 + if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) { 1.1842 + if (node.attributes.length == 0) { 1.1843 + return true; 1.1844 + } 1.1845 + 1.1846 + if (node.attributes.length == 1 1.1847 + && node.hasAttribute("style")) { 1.1848 + return true; 1.1849 + } 1.1850 + } 1.1851 + 1.1852 + if (node.tagName == "FONT" || node.tagName == "A") { 1.1853 + var numAttrs = node.attributes.length; 1.1854 + 1.1855 + if (node.hasAttribute("style")) { 1.1856 + numAttrs--; 1.1857 + } 1.1858 + 1.1859 + if (node.tagName == "FONT") { 1.1860 + if (node.hasAttribute("color")) { 1.1861 + numAttrs--; 1.1862 + } 1.1863 + 1.1864 + if (node.hasAttribute("face")) { 1.1865 + numAttrs--; 1.1866 + } 1.1867 + 1.1868 + if (node.hasAttribute("size")) { 1.1869 + numAttrs--; 1.1870 + } 1.1871 + } 1.1872 + 1.1873 + if (node.tagName == "A" 1.1874 + && node.hasAttribute("href")) { 1.1875 + numAttrs--; 1.1876 + } 1.1877 + 1.1878 + if (numAttrs == 0) { 1.1879 + return true; 1.1880 + } 1.1881 + } 1.1882 + 1.1883 + return false; 1.1884 +} 1.1885 + 1.1886 +function isSimpleModifiableElement(node) { 1.1887 + // "A simple modifiable element is an HTML element for which at least one 1.1888 + // of the following holds:" 1.1889 + if (!isHtmlElement(node)) { 1.1890 + return false; 1.1891 + } 1.1892 + 1.1893 + // Only these elements can possibly be a simple modifiable element. 1.1894 + if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) { 1.1895 + return false; 1.1896 + } 1.1897 + 1.1898 + // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 1.1899 + // element with no attributes." 1.1900 + if (node.attributes.length == 0) { 1.1901 + return true; 1.1902 + } 1.1903 + 1.1904 + // If it's got more than one attribute, everything after this fails. 1.1905 + if (node.attributes.length > 1) { 1.1906 + return false; 1.1907 + } 1.1908 + 1.1909 + // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 1.1910 + // element with exactly one attribute, which is style, which sets no CSS 1.1911 + // properties (including invalid or unrecognized properties)." 1.1912 + // 1.1913 + // Not gonna try for invalid or unrecognized. 1.1914 + if (node.hasAttribute("style") 1.1915 + && node.style.length == 0) { 1.1916 + return true; 1.1917 + } 1.1918 + 1.1919 + // "It is an a element with exactly one attribute, which is href." 1.1920 + if (node.tagName == "A" 1.1921 + && node.hasAttribute("href")) { 1.1922 + return true; 1.1923 + } 1.1924 + 1.1925 + // "It is a font element with exactly one attribute, which is either color, 1.1926 + // face, or size." 1.1927 + if (node.tagName == "FONT" 1.1928 + && (node.hasAttribute("color") 1.1929 + || node.hasAttribute("face") 1.1930 + || node.hasAttribute("size") 1.1931 + )) { 1.1932 + return true; 1.1933 + } 1.1934 + 1.1935 + // "It is a b or strong element with exactly one attribute, which is style, 1.1936 + // and the style attribute sets exactly one CSS property (including invalid 1.1937 + // or unrecognized properties), which is "font-weight"." 1.1938 + if ((node.tagName == "B" || node.tagName == "STRONG") 1.1939 + && node.hasAttribute("style") 1.1940 + && node.style.length == 1 1.1941 + && node.style.fontWeight != "") { 1.1942 + return true; 1.1943 + } 1.1944 + 1.1945 + // "It is an i or em element with exactly one attribute, which is style, 1.1946 + // and the style attribute sets exactly one CSS property (including invalid 1.1947 + // or unrecognized properties), which is "font-style"." 1.1948 + if ((node.tagName == "I" || node.tagName == "EM") 1.1949 + && node.hasAttribute("style") 1.1950 + && node.style.length == 1 1.1951 + && node.style.fontStyle != "") { 1.1952 + return true; 1.1953 + } 1.1954 + 1.1955 + // "It is an a, font, or span element with exactly one attribute, which is 1.1956 + // style, and the style attribute sets exactly one CSS property (including 1.1957 + // invalid or unrecognized properties), and that property is not 1.1958 + // "text-decoration"." 1.1959 + if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN") 1.1960 + && node.hasAttribute("style") 1.1961 + && node.style.length == 1 1.1962 + && node.style.textDecoration == "") { 1.1963 + return true; 1.1964 + } 1.1965 + 1.1966 + // "It is an a, font, s, span, strike, or u element with exactly one 1.1967 + // attribute, which is style, and the style attribute sets exactly one CSS 1.1968 + // property (including invalid or unrecognized properties), which is 1.1969 + // "text-decoration", which is set to "line-through" or "underline" or 1.1970 + // "overline" or "none"." 1.1971 + // 1.1972 + // The weird extra node.style.length check is for Firefox, which as of 1.1973 + // 8.0a2 has annoying and weird behavior here. 1.1974 + if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1 1.1975 + && node.hasAttribute("style") 1.1976 + && (node.style.length == 1 1.1977 + || (node.style.length == 4 1.1978 + && "MozTextBlink" in node.style 1.1979 + && "MozTextDecorationColor" in node.style 1.1980 + && "MozTextDecorationLine" in node.style 1.1981 + && "MozTextDecorationStyle" in node.style) 1.1982 + ) 1.1983 + && (node.style.textDecoration == "line-through" 1.1984 + || node.style.textDecoration == "underline" 1.1985 + || node.style.textDecoration == "overline" 1.1986 + || node.style.textDecoration == "none")) { 1.1987 + return true; 1.1988 + } 1.1989 + 1.1990 + return false; 1.1991 +} 1.1992 + 1.1993 +// "A formattable node is an editable visible node that is either a Text node, 1.1994 +// an img, or a br." 1.1995 +function isFormattableNode(node) { 1.1996 + return isEditable(node) 1.1997 + && isVisible(node) 1.1998 + && (node.nodeType == Node.TEXT_NODE 1.1999 + || isHtmlElement(node, ["img", "br"])); 1.2000 +} 1.2001 + 1.2002 +// "Two quantities are equivalent values for a command if either both are null, 1.2003 +// or both are strings and they're equal and the command does not define any 1.2004 +// equivalent values, or both are strings and the command defines equivalent 1.2005 +// values and they match the definition." 1.2006 +function areEquivalentValues(command, val1, val2) { 1.2007 + if (val1 === null && val2 === null) { 1.2008 + return true; 1.2009 + } 1.2010 + 1.2011 + if (typeof val1 == "string" 1.2012 + && typeof val2 == "string" 1.2013 + && val1 == val2 1.2014 + && !("equivalentValues" in commands[command])) { 1.2015 + return true; 1.2016 + } 1.2017 + 1.2018 + if (typeof val1 == "string" 1.2019 + && typeof val2 == "string" 1.2020 + && "equivalentValues" in commands[command] 1.2021 + && commands[command].equivalentValues(val1, val2)) { 1.2022 + return true; 1.2023 + } 1.2024 + 1.2025 + return false; 1.2026 +} 1.2027 + 1.2028 +// "Two quantities are loosely equivalent values for a command if either they 1.2029 +// are equivalent values for the command, or if the command is the fontSize 1.2030 +// command; one of the quantities is one of "x-small", "small", "medium", 1.2031 +// "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is 1.2032 +// the resolved value of "font-size" on a font element whose size attribute has 1.2033 +// the corresponding value set ("1" through "7" respectively)." 1.2034 +function areLooselyEquivalentValues(command, val1, val2) { 1.2035 + if (areEquivalentValues(command, val1, val2)) { 1.2036 + return true; 1.2037 + } 1.2038 + 1.2039 + if (command != "fontsize" 1.2040 + || typeof val1 != "string" 1.2041 + || typeof val2 != "string") { 1.2042 + return false; 1.2043 + } 1.2044 + 1.2045 + // Static variables in JavaScript? 1.2046 + var callee = areLooselyEquivalentValues; 1.2047 + if (callee.sizeMap === undefined) { 1.2048 + callee.sizeMap = {}; 1.2049 + var font = document.createElement("font"); 1.2050 + document.body.appendChild(font); 1.2051 + ["x-small", "small", "medium", "large", "x-large", "xx-large", 1.2052 + "xxx-large"].forEach(function(keyword) { 1.2053 + font.size = cssSizeToLegacy(keyword); 1.2054 + callee.sizeMap[keyword] = getComputedStyle(font).fontSize; 1.2055 + }); 1.2056 + document.body.removeChild(font); 1.2057 + } 1.2058 + 1.2059 + return val1 === callee.sizeMap[val2] 1.2060 + || val2 === callee.sizeMap[val1]; 1.2061 +} 1.2062 + 1.2063 +//@} 1.2064 +///// Assorted inline formatting command algorithms ///// 1.2065 +//@{ 1.2066 + 1.2067 +function getEffectiveCommandValue(node, command) { 1.2068 + // "If neither node nor its parent is an Element, return null." 1.2069 + if (node.nodeType != Node.ELEMENT_NODE 1.2070 + && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) { 1.2071 + return null; 1.2072 + } 1.2073 + 1.2074 + // "If node is not an Element, return the effective command value of its 1.2075 + // parent for command." 1.2076 + if (node.nodeType != Node.ELEMENT_NODE) { 1.2077 + return getEffectiveCommandValue(node.parentNode, command); 1.2078 + } 1.2079 + 1.2080 + // "If command is "createLink" or "unlink":" 1.2081 + if (command == "createlink" || command == "unlink") { 1.2082 + // "While node is not null, and is not an a element that has an href 1.2083 + // attribute, set node to its parent." 1.2084 + while (node 1.2085 + && (!isHtmlElement(node) 1.2086 + || node.tagName != "A" 1.2087 + || !node.hasAttribute("href"))) { 1.2088 + node = node.parentNode; 1.2089 + } 1.2090 + 1.2091 + // "If node is null, return null." 1.2092 + if (!node) { 1.2093 + return null; 1.2094 + } 1.2095 + 1.2096 + // "Return the value of node's href attribute." 1.2097 + return node.getAttribute("href"); 1.2098 + } 1.2099 + 1.2100 + // "If command is "backColor" or "hiliteColor":" 1.2101 + if (command == "backcolor" 1.2102 + || command == "hilitecolor") { 1.2103 + // "While the resolved value of "background-color" on node is any 1.2104 + // fully transparent value, and node's parent is an Element, set 1.2105 + // node to its parent." 1.2106 + // 1.2107 + // Another lame hack to avoid flawed APIs. 1.2108 + while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" 1.2109 + || getComputedStyle(node).backgroundColor === "" 1.2110 + || getComputedStyle(node).backgroundColor == "transparent") 1.2111 + && node.parentNode 1.2112 + && node.parentNode.nodeType == Node.ELEMENT_NODE) { 1.2113 + node = node.parentNode; 1.2114 + } 1.2115 + 1.2116 + // "Return the resolved value of "background-color" for node." 1.2117 + return getComputedStyle(node).backgroundColor; 1.2118 + } 1.2119 + 1.2120 + // "If command is "subscript" or "superscript":" 1.2121 + if (command == "subscript" || command == "superscript") { 1.2122 + // "Let affected by subscript and affected by superscript be two 1.2123 + // boolean variables, both initially false." 1.2124 + var affectedBySubscript = false; 1.2125 + var affectedBySuperscript = false; 1.2126 + 1.2127 + // "While node is an inline node:" 1.2128 + while (isInlineNode(node)) { 1.2129 + var verticalAlign = getComputedStyle(node).verticalAlign; 1.2130 + 1.2131 + // "If node is a sub, set affected by subscript to true." 1.2132 + if (isHtmlElement(node, "sub")) { 1.2133 + affectedBySubscript = true; 1.2134 + // "Otherwise, if node is a sup, set affected by superscript to 1.2135 + // true." 1.2136 + } else if (isHtmlElement(node, "sup")) { 1.2137 + affectedBySuperscript = true; 1.2138 + } 1.2139 + 1.2140 + // "Set node to its parent." 1.2141 + node = node.parentNode; 1.2142 + } 1.2143 + 1.2144 + // "If affected by subscript and affected by superscript are both true, 1.2145 + // return the string "mixed"." 1.2146 + if (affectedBySubscript && affectedBySuperscript) { 1.2147 + return "mixed"; 1.2148 + } 1.2149 + 1.2150 + // "If affected by subscript is true, return "subscript"." 1.2151 + if (affectedBySubscript) { 1.2152 + return "subscript"; 1.2153 + } 1.2154 + 1.2155 + // "If affected by superscript is true, return "superscript"." 1.2156 + if (affectedBySuperscript) { 1.2157 + return "superscript"; 1.2158 + } 1.2159 + 1.2160 + // "Return null." 1.2161 + return null; 1.2162 + } 1.2163 + 1.2164 + // "If command is "strikethrough", and the "text-decoration" property of 1.2165 + // node or any of its ancestors has resolved value containing 1.2166 + // "line-through", return "line-through". Otherwise, return null." 1.2167 + if (command == "strikethrough") { 1.2168 + do { 1.2169 + if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) { 1.2170 + return "line-through"; 1.2171 + } 1.2172 + node = node.parentNode; 1.2173 + } while (node && node.nodeType == Node.ELEMENT_NODE); 1.2174 + return null; 1.2175 + } 1.2176 + 1.2177 + // "If command is "underline", and the "text-decoration" property of node 1.2178 + // or any of its ancestors has resolved value containing "underline", 1.2179 + // return "underline". Otherwise, return null." 1.2180 + if (command == "underline") { 1.2181 + do { 1.2182 + if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) { 1.2183 + return "underline"; 1.2184 + } 1.2185 + node = node.parentNode; 1.2186 + } while (node && node.nodeType == Node.ELEMENT_NODE); 1.2187 + return null; 1.2188 + } 1.2189 + 1.2190 + if (!("relevantCssProperty" in commands[command])) { 1.2191 + throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue"; 1.2192 + } 1.2193 + 1.2194 + // "Return the resolved value for node of the relevant CSS property for 1.2195 + // command." 1.2196 + return getComputedStyle(node)[commands[command].relevantCssProperty]; 1.2197 +} 1.2198 + 1.2199 +function getSpecifiedCommandValue(element, command) { 1.2200 + // "If command is "backColor" or "hiliteColor" and element's display 1.2201 + // property does not have resolved value "inline", return null." 1.2202 + if ((command == "backcolor" || command == "hilitecolor") 1.2203 + && getComputedStyle(element).display != "inline") { 1.2204 + return null; 1.2205 + } 1.2206 + 1.2207 + // "If command is "createLink" or "unlink":" 1.2208 + if (command == "createlink" || command == "unlink") { 1.2209 + // "If element is an a element and has an href attribute, return the 1.2210 + // value of that attribute." 1.2211 + if (isHtmlElement(element) 1.2212 + && element.tagName == "A" 1.2213 + && element.hasAttribute("href")) { 1.2214 + return element.getAttribute("href"); 1.2215 + } 1.2216 + 1.2217 + // "Return null." 1.2218 + return null; 1.2219 + } 1.2220 + 1.2221 + // "If command is "subscript" or "superscript":" 1.2222 + if (command == "subscript" || command == "superscript") { 1.2223 + // "If element is a sup, return "superscript"." 1.2224 + if (isHtmlElement(element, "sup")) { 1.2225 + return "superscript"; 1.2226 + } 1.2227 + 1.2228 + // "If element is a sub, return "subscript"." 1.2229 + if (isHtmlElement(element, "sub")) { 1.2230 + return "subscript"; 1.2231 + } 1.2232 + 1.2233 + // "Return null." 1.2234 + return null; 1.2235 + } 1.2236 + 1.2237 + // "If command is "strikethrough", and element has a style attribute set, 1.2238 + // and that attribute sets "text-decoration":" 1.2239 + if (command == "strikethrough" 1.2240 + && element.style.textDecoration != "") { 1.2241 + // "If element's style attribute sets "text-decoration" to a value 1.2242 + // containing "line-through", return "line-through"." 1.2243 + if (element.style.textDecoration.indexOf("line-through") != -1) { 1.2244 + return "line-through"; 1.2245 + } 1.2246 + 1.2247 + // "Return null." 1.2248 + return null; 1.2249 + } 1.2250 + 1.2251 + // "If command is "strikethrough" and element is a s or strike element, 1.2252 + // return "line-through"." 1.2253 + if (command == "strikethrough" 1.2254 + && isHtmlElement(element, ["S", "STRIKE"])) { 1.2255 + return "line-through"; 1.2256 + } 1.2257 + 1.2258 + // "If command is "underline", and element has a style attribute set, and 1.2259 + // that attribute sets "text-decoration":" 1.2260 + if (command == "underline" 1.2261 + && element.style.textDecoration != "") { 1.2262 + // "If element's style attribute sets "text-decoration" to a value 1.2263 + // containing "underline", return "underline"." 1.2264 + if (element.style.textDecoration.indexOf("underline") != -1) { 1.2265 + return "underline"; 1.2266 + } 1.2267 + 1.2268 + // "Return null." 1.2269 + return null; 1.2270 + } 1.2271 + 1.2272 + // "If command is "underline" and element is a u element, return 1.2273 + // "underline"." 1.2274 + if (command == "underline" 1.2275 + && isHtmlElement(element, "U")) { 1.2276 + return "underline"; 1.2277 + } 1.2278 + 1.2279 + // "Let property be the relevant CSS property for command." 1.2280 + var property = commands[command].relevantCssProperty; 1.2281 + 1.2282 + // "If property is null, return null." 1.2283 + if (property === null) { 1.2284 + return null; 1.2285 + } 1.2286 + 1.2287 + // "If element has a style attribute set, and that attribute has the 1.2288 + // effect of setting property, return the value that it sets property to." 1.2289 + if (element.style[property] != "") { 1.2290 + return element.style[property]; 1.2291 + } 1.2292 + 1.2293 + // "If element is a font element that has an attribute whose effect is 1.2294 + // to create a presentational hint for property, return the value that the 1.2295 + // hint sets property to. (For a size of 7, this will be the non-CSS value 1.2296 + // "xxx-large".)" 1.2297 + if (isHtmlNamespace(element.namespaceURI) 1.2298 + && element.tagName == "FONT") { 1.2299 + if (property == "color" && element.hasAttribute("color")) { 1.2300 + return element.color; 1.2301 + } 1.2302 + if (property == "fontFamily" && element.hasAttribute("face")) { 1.2303 + return element.face; 1.2304 + } 1.2305 + if (property == "fontSize" && element.hasAttribute("size")) { 1.2306 + // This is not even close to correct in general. 1.2307 + var size = parseInt(element.size); 1.2308 + if (size < 1) { 1.2309 + size = 1; 1.2310 + } 1.2311 + if (size > 7) { 1.2312 + size = 7; 1.2313 + } 1.2314 + return { 1.2315 + 1: "x-small", 1.2316 + 2: "small", 1.2317 + 3: "medium", 1.2318 + 4: "large", 1.2319 + 5: "x-large", 1.2320 + 6: "xx-large", 1.2321 + 7: "xxx-large" 1.2322 + }[size]; 1.2323 + } 1.2324 + } 1.2325 + 1.2326 + // "If element is in the following list, and property is equal to the 1.2327 + // CSS property name listed for it, return the string listed for it." 1.2328 + // 1.2329 + // A list follows, whose meaning is copied here. 1.2330 + if (property == "fontWeight" 1.2331 + && (element.tagName == "B" || element.tagName == "STRONG")) { 1.2332 + return "bold"; 1.2333 + } 1.2334 + if (property == "fontStyle" 1.2335 + && (element.tagName == "I" || element.tagName == "EM")) { 1.2336 + return "italic"; 1.2337 + } 1.2338 + 1.2339 + // "Return null." 1.2340 + return null; 1.2341 +} 1.2342 + 1.2343 +function reorderModifiableDescendants(node, command, newValue) { 1.2344 + // "Let candidate equal node." 1.2345 + var candidate = node; 1.2346 + 1.2347 + // "While candidate is a modifiable element, and candidate has exactly one 1.2348 + // child, and that child is also a modifiable element, and candidate is not 1.2349 + // a simple modifiable element or candidate's specified command value for 1.2350 + // command is not equivalent to new value, set candidate to its child." 1.2351 + while (isModifiableElement(candidate) 1.2352 + && candidate.childNodes.length == 1 1.2353 + && isModifiableElement(candidate.firstChild) 1.2354 + && (!isSimpleModifiableElement(candidate) 1.2355 + || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) { 1.2356 + candidate = candidate.firstChild; 1.2357 + } 1.2358 + 1.2359 + // "If candidate is node, or is not a simple modifiable element, or its 1.2360 + // specified command value is not equivalent to new value, or its effective 1.2361 + // command value is not loosely equivalent to new value, abort these 1.2362 + // steps." 1.2363 + if (candidate == node 1.2364 + || !isSimpleModifiableElement(candidate) 1.2365 + || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue) 1.2366 + || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) { 1.2367 + return; 1.2368 + } 1.2369 + 1.2370 + // "While candidate has children, insert the first child of candidate into 1.2371 + // candidate's parent immediately before candidate, preserving ranges." 1.2372 + while (candidate.hasChildNodes()) { 1.2373 + movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate)); 1.2374 + } 1.2375 + 1.2376 + // "Insert candidate into node's parent immediately after node." 1.2377 + node.parentNode.insertBefore(candidate, node.nextSibling); 1.2378 + 1.2379 + // "Append the node as the last child of candidate, preserving ranges." 1.2380 + movePreservingRanges(node, candidate, -1); 1.2381 +} 1.2382 + 1.2383 +function recordValues(nodeList) { 1.2384 + // "Let values be a list of (node, command, specified command value) 1.2385 + // triples, initially empty." 1.2386 + var values = []; 1.2387 + 1.2388 + // "For each node in node list, for each command in the list "subscript", 1.2389 + // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", 1.2390 + // "strikethrough", and "underline" in that order:" 1.2391 + nodeList.forEach(function(node) { 1.2392 + ["subscript", "bold", "fontname", "fontsize", "forecolor", 1.2393 + "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) { 1.2394 + // "Let ancestor equal node." 1.2395 + var ancestor = node; 1.2396 + 1.2397 + // "If ancestor is not an Element, set it to its parent." 1.2398 + if (ancestor.nodeType != Node.ELEMENT_NODE) { 1.2399 + ancestor = ancestor.parentNode; 1.2400 + } 1.2401 + 1.2402 + // "While ancestor is an Element and its specified command value 1.2403 + // for command is null, set it to its parent." 1.2404 + while (ancestor 1.2405 + && ancestor.nodeType == Node.ELEMENT_NODE 1.2406 + && getSpecifiedCommandValue(ancestor, command) === null) { 1.2407 + ancestor = ancestor.parentNode; 1.2408 + } 1.2409 + 1.2410 + // "If ancestor is an Element, add (node, command, ancestor's 1.2411 + // specified command value for command) to values. Otherwise add 1.2412 + // (node, command, null) to values." 1.2413 + if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) { 1.2414 + values.push([node, command, getSpecifiedCommandValue(ancestor, command)]); 1.2415 + } else { 1.2416 + values.push([node, command, null]); 1.2417 + } 1.2418 + }); 1.2419 + }); 1.2420 + 1.2421 + // "Return values." 1.2422 + return values; 1.2423 +} 1.2424 + 1.2425 +function restoreValues(values) { 1.2426 + // "For each (node, command, value) triple in values:" 1.2427 + values.forEach(function(triple) { 1.2428 + var node = triple[0]; 1.2429 + var command = triple[1]; 1.2430 + var value = triple[2]; 1.2431 + 1.2432 + // "Let ancestor equal node." 1.2433 + var ancestor = node; 1.2434 + 1.2435 + // "If ancestor is not an Element, set it to its parent." 1.2436 + if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { 1.2437 + ancestor = ancestor.parentNode; 1.2438 + } 1.2439 + 1.2440 + // "While ancestor is an Element and its specified command value for 1.2441 + // command is null, set it to its parent." 1.2442 + while (ancestor 1.2443 + && ancestor.nodeType == Node.ELEMENT_NODE 1.2444 + && getSpecifiedCommandValue(ancestor, command) === null) { 1.2445 + ancestor = ancestor.parentNode; 1.2446 + } 1.2447 + 1.2448 + // "If value is null and ancestor is an Element, push down values on 1.2449 + // node for command, with new value null." 1.2450 + if (value === null 1.2451 + && ancestor 1.2452 + && ancestor.nodeType == Node.ELEMENT_NODE) { 1.2453 + pushDownValues(node, command, null); 1.2454 + 1.2455 + // "Otherwise, if ancestor is an Element and its specified command 1.2456 + // value for command is not equivalent to value, or if ancestor is not 1.2457 + // an Element and value is not null, force the value of command to 1.2458 + // value on node." 1.2459 + } else if ((ancestor 1.2460 + && ancestor.nodeType == Node.ELEMENT_NODE 1.2461 + && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value)) 1.2462 + || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) 1.2463 + && value !== null)) { 1.2464 + forceValue(node, command, value); 1.2465 + } 1.2466 + }); 1.2467 +} 1.2468 + 1.2469 + 1.2470 +//@} 1.2471 +///// Clearing an element's value ///// 1.2472 +//@{ 1.2473 + 1.2474 +function clearValue(element, command) { 1.2475 + // "If element is not editable, return the empty list." 1.2476 + if (!isEditable(element)) { 1.2477 + return []; 1.2478 + } 1.2479 + 1.2480 + // "If element's specified command value for command is null, return the 1.2481 + // empty list." 1.2482 + if (getSpecifiedCommandValue(element, command) === null) { 1.2483 + return []; 1.2484 + } 1.2485 + 1.2486 + // "If element is a simple modifiable element:" 1.2487 + if (isSimpleModifiableElement(element)) { 1.2488 + // "Let children be the children of element." 1.2489 + var children = Array.prototype.slice.call(element.childNodes); 1.2490 + 1.2491 + // "For each child in children, insert child into element's parent 1.2492 + // immediately before element, preserving ranges." 1.2493 + for (var i = 0; i < children.length; i++) { 1.2494 + movePreservingRanges(children[i], element.parentNode, getNodeIndex(element)); 1.2495 + } 1.2496 + 1.2497 + // "Remove element from its parent." 1.2498 + element.parentNode.removeChild(element); 1.2499 + 1.2500 + // "Return children." 1.2501 + return children; 1.2502 + } 1.2503 + 1.2504 + // "If command is "strikethrough", and element has a style attribute that 1.2505 + // sets "text-decoration" to some value containing "line-through", delete 1.2506 + // "line-through" from the value." 1.2507 + if (command == "strikethrough" 1.2508 + && element.style.textDecoration.indexOf("line-through") != -1) { 1.2509 + if (element.style.textDecoration == "line-through") { 1.2510 + element.style.textDecoration = ""; 1.2511 + } else { 1.2512 + element.style.textDecoration = element.style.textDecoration.replace("line-through", ""); 1.2513 + } 1.2514 + if (element.getAttribute("style") == "") { 1.2515 + element.removeAttribute("style"); 1.2516 + } 1.2517 + } 1.2518 + 1.2519 + // "If command is "underline", and element has a style attribute that sets 1.2520 + // "text-decoration" to some value containing "underline", delete 1.2521 + // "underline" from the value." 1.2522 + if (command == "underline" 1.2523 + && element.style.textDecoration.indexOf("underline") != -1) { 1.2524 + if (element.style.textDecoration == "underline") { 1.2525 + element.style.textDecoration = ""; 1.2526 + } else { 1.2527 + element.style.textDecoration = element.style.textDecoration.replace("underline", ""); 1.2528 + } 1.2529 + if (element.getAttribute("style") == "") { 1.2530 + element.removeAttribute("style"); 1.2531 + } 1.2532 + } 1.2533 + 1.2534 + // "If the relevant CSS property for command is not null, unset the CSS 1.2535 + // property property of element." 1.2536 + if (commands[command].relevantCssProperty !== null) { 1.2537 + element.style[commands[command].relevantCssProperty] = ''; 1.2538 + if (element.getAttribute("style") == "") { 1.2539 + element.removeAttribute("style"); 1.2540 + } 1.2541 + } 1.2542 + 1.2543 + // "If element is a font element:" 1.2544 + if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { 1.2545 + // "If command is "foreColor", unset element's color attribute, if set." 1.2546 + if (command == "forecolor") { 1.2547 + element.removeAttribute("color"); 1.2548 + } 1.2549 + 1.2550 + // "If command is "fontName", unset element's face attribute, if set." 1.2551 + if (command == "fontname") { 1.2552 + element.removeAttribute("face"); 1.2553 + } 1.2554 + 1.2555 + // "If command is "fontSize", unset element's size attribute, if set." 1.2556 + if (command == "fontsize") { 1.2557 + element.removeAttribute("size"); 1.2558 + } 1.2559 + } 1.2560 + 1.2561 + // "If element is an a element and command is "createLink" or "unlink", 1.2562 + // unset the href property of element." 1.2563 + if (isHtmlElement(element, "A") 1.2564 + && (command == "createlink" || command == "unlink")) { 1.2565 + element.removeAttribute("href"); 1.2566 + } 1.2567 + 1.2568 + // "If element's specified command value for command is null, return the 1.2569 + // empty list." 1.2570 + if (getSpecifiedCommandValue(element, command) === null) { 1.2571 + return []; 1.2572 + } 1.2573 + 1.2574 + // "Set the tag name of element to "span", and return the one-node list 1.2575 + // consisting of the result." 1.2576 + return [setTagName(element, "span")]; 1.2577 +} 1.2578 + 1.2579 + 1.2580 +//@} 1.2581 +///// Pushing down values ///// 1.2582 +//@{ 1.2583 + 1.2584 +function pushDownValues(node, command, newValue) { 1.2585 + // "If node's parent is not an Element, abort this algorithm." 1.2586 + if (!node.parentNode 1.2587 + || node.parentNode.nodeType != Node.ELEMENT_NODE) { 1.2588 + return; 1.2589 + } 1.2590 + 1.2591 + // "If the effective command value of command is loosely equivalent to new 1.2592 + // value on node, abort this algorithm." 1.2593 + if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 1.2594 + return; 1.2595 + } 1.2596 + 1.2597 + // "Let current ancestor be node's parent." 1.2598 + var currentAncestor = node.parentNode; 1.2599 + 1.2600 + // "Let ancestor list be a list of Nodes, initially empty." 1.2601 + var ancestorList = []; 1.2602 + 1.2603 + // "While current ancestor is an editable Element and the effective command 1.2604 + // value of command is not loosely equivalent to new value on it, append 1.2605 + // current ancestor to ancestor list, then set current ancestor to its 1.2606 + // parent." 1.2607 + while (isEditable(currentAncestor) 1.2608 + && currentAncestor.nodeType == Node.ELEMENT_NODE 1.2609 + && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) { 1.2610 + ancestorList.push(currentAncestor); 1.2611 + currentAncestor = currentAncestor.parentNode; 1.2612 + } 1.2613 + 1.2614 + // "If ancestor list is empty, abort this algorithm." 1.2615 + if (!ancestorList.length) { 1.2616 + return; 1.2617 + } 1.2618 + 1.2619 + // "Let propagated value be the specified command value of command on the 1.2620 + // last member of ancestor list." 1.2621 + var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command); 1.2622 + 1.2623 + // "If propagated value is null and is not equal to new value, abort this 1.2624 + // algorithm." 1.2625 + if (propagatedValue === null && propagatedValue != newValue) { 1.2626 + return; 1.2627 + } 1.2628 + 1.2629 + // "If the effective command value for the parent of the last member of 1.2630 + // ancestor list is not loosely equivalent to new value, and new value is 1.2631 + // not null, abort this algorithm." 1.2632 + if (newValue !== null 1.2633 + && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) { 1.2634 + return; 1.2635 + } 1.2636 + 1.2637 + // "While ancestor list is not empty:" 1.2638 + while (ancestorList.length) { 1.2639 + // "Let current ancestor be the last member of ancestor list." 1.2640 + // "Remove the last member from ancestor list." 1.2641 + var currentAncestor = ancestorList.pop(); 1.2642 + 1.2643 + // "If the specified command value of current ancestor for command is 1.2644 + // not null, set propagated value to that value." 1.2645 + if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 1.2646 + propagatedValue = getSpecifiedCommandValue(currentAncestor, command); 1.2647 + } 1.2648 + 1.2649 + // "Let children be the children of current ancestor." 1.2650 + var children = Array.prototype.slice.call(currentAncestor.childNodes); 1.2651 + 1.2652 + // "If the specified command value of current ancestor for command is 1.2653 + // not null, clear the value of current ancestor." 1.2654 + if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 1.2655 + clearValue(currentAncestor, command); 1.2656 + } 1.2657 + 1.2658 + // "For every child in children:" 1.2659 + for (var i = 0; i < children.length; i++) { 1.2660 + var child = children[i]; 1.2661 + 1.2662 + // "If child is node, continue with the next child." 1.2663 + if (child == node) { 1.2664 + continue; 1.2665 + } 1.2666 + 1.2667 + // "If child is an Element whose specified command value for 1.2668 + // command is neither null nor equivalent to propagated value, 1.2669 + // continue with the next child." 1.2670 + if (child.nodeType == Node.ELEMENT_NODE 1.2671 + && getSpecifiedCommandValue(child, command) !== null 1.2672 + && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) { 1.2673 + continue; 1.2674 + } 1.2675 + 1.2676 + // "If child is the last member of ancestor list, continue with the 1.2677 + // next child." 1.2678 + if (child == ancestorList[ancestorList.length - 1]) { 1.2679 + continue; 1.2680 + } 1.2681 + 1.2682 + // "Force the value of child, with command as in this algorithm 1.2683 + // and new value equal to propagated value." 1.2684 + forceValue(child, command, propagatedValue); 1.2685 + } 1.2686 + } 1.2687 +} 1.2688 + 1.2689 + 1.2690 +//@} 1.2691 +///// Forcing the value of a node ///// 1.2692 +//@{ 1.2693 + 1.2694 +function forceValue(node, command, newValue) { 1.2695 + // "If node's parent is null, abort this algorithm." 1.2696 + if (!node.parentNode) { 1.2697 + return; 1.2698 + } 1.2699 + 1.2700 + // "If new value is null, abort this algorithm." 1.2701 + if (newValue === null) { 1.2702 + return; 1.2703 + } 1.2704 + 1.2705 + // "If node is an allowed child of "span":" 1.2706 + if (isAllowedChild(node, "span")) { 1.2707 + // "Reorder modifiable descendants of node's previousSibling." 1.2708 + reorderModifiableDescendants(node.previousSibling, command, newValue); 1.2709 + 1.2710 + // "Reorder modifiable descendants of node's nextSibling." 1.2711 + reorderModifiableDescendants(node.nextSibling, command, newValue); 1.2712 + 1.2713 + // "Wrap the one-node list consisting of node, with sibling criteria 1.2714 + // returning true for a simple modifiable element whose specified 1.2715 + // command value is equivalent to new value and whose effective command 1.2716 + // value is loosely equivalent to new value and false otherwise, and 1.2717 + // with new parent instructions returning null." 1.2718 + wrap([node], 1.2719 + function(node) { 1.2720 + return isSimpleModifiableElement(node) 1.2721 + && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue) 1.2722 + && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue); 1.2723 + }, 1.2724 + function() { return null } 1.2725 + ); 1.2726 + } 1.2727 + 1.2728 + // "If node is invisible, abort this algorithm." 1.2729 + if (isInvisible(node)) { 1.2730 + return; 1.2731 + } 1.2732 + 1.2733 + // "If the effective command value of command is loosely equivalent to new 1.2734 + // value on node, abort this algorithm." 1.2735 + if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 1.2736 + return; 1.2737 + } 1.2738 + 1.2739 + // "If node is not an allowed child of "span":" 1.2740 + if (!isAllowedChild(node, "span")) { 1.2741 + // "Let children be all children of node, omitting any that are 1.2742 + // Elements whose specified command value for command is neither null 1.2743 + // nor equivalent to new value." 1.2744 + var children = []; 1.2745 + for (var i = 0; i < node.childNodes.length; i++) { 1.2746 + if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { 1.2747 + var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 1.2748 + 1.2749 + if (specifiedValue !== null 1.2750 + && !areEquivalentValues(command, newValue, specifiedValue)) { 1.2751 + continue; 1.2752 + } 1.2753 + } 1.2754 + children.push(node.childNodes[i]); 1.2755 + } 1.2756 + 1.2757 + // "Force the value of each Node in children, with command and new 1.2758 + // value as in this invocation of the algorithm." 1.2759 + for (var i = 0; i < children.length; i++) { 1.2760 + forceValue(children[i], command, newValue); 1.2761 + } 1.2762 + 1.2763 + // "Abort this algorithm." 1.2764 + return; 1.2765 + } 1.2766 + 1.2767 + // "If the effective command value of command is loosely equivalent to new 1.2768 + // value on node, abort this algorithm." 1.2769 + if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 1.2770 + return; 1.2771 + } 1.2772 + 1.2773 + // "Let new parent be null." 1.2774 + var newParent = null; 1.2775 + 1.2776 + // "If the CSS styling flag is false:" 1.2777 + if (!cssStylingFlag) { 1.2778 + // "If command is "bold" and new value is "bold", let new parent be the 1.2779 + // result of calling createElement("b") on the ownerDocument of node." 1.2780 + if (command == "bold" && (newValue == "bold" || newValue == "700")) { 1.2781 + newParent = node.ownerDocument.createElement("b"); 1.2782 + } 1.2783 + 1.2784 + // "If command is "italic" and new value is "italic", let new parent be 1.2785 + // the result of calling createElement("i") on the ownerDocument of 1.2786 + // node." 1.2787 + if (command == "italic" && newValue == "italic") { 1.2788 + newParent = node.ownerDocument.createElement("i"); 1.2789 + } 1.2790 + 1.2791 + // "If command is "strikethrough" and new value is "line-through", let 1.2792 + // new parent be the result of calling createElement("s") on the 1.2793 + // ownerDocument of node." 1.2794 + if (command == "strikethrough" && newValue == "line-through") { 1.2795 + newParent = node.ownerDocument.createElement("s"); 1.2796 + } 1.2797 + 1.2798 + // "If command is "underline" and new value is "underline", let new 1.2799 + // parent be the result of calling createElement("u") on the 1.2800 + // ownerDocument of node." 1.2801 + if (command == "underline" && newValue == "underline") { 1.2802 + newParent = node.ownerDocument.createElement("u"); 1.2803 + } 1.2804 + 1.2805 + // "If command is "foreColor", and new value is fully opaque with red, 1.2806 + // green, and blue components in the range 0 to 255:" 1.2807 + if (command == "forecolor" && parseSimpleColor(newValue)) { 1.2808 + // "Let new parent be the result of calling createElement("font") 1.2809 + // on the ownerDocument of node." 1.2810 + newParent = node.ownerDocument.createElement("font"); 1.2811 + 1.2812 + // "Set the color attribute of new parent to the result of applying 1.2813 + // the rules for serializing simple color values to new value 1.2814 + // (interpreted as a simple color)." 1.2815 + newParent.setAttribute("color", parseSimpleColor(newValue)); 1.2816 + } 1.2817 + 1.2818 + // "If command is "fontName", let new parent be the result of calling 1.2819 + // createElement("font") on the ownerDocument of node, then set the 1.2820 + // face attribute of new parent to new value." 1.2821 + if (command == "fontname") { 1.2822 + newParent = node.ownerDocument.createElement("font"); 1.2823 + newParent.face = newValue; 1.2824 + } 1.2825 + } 1.2826 + 1.2827 + // "If command is "createLink" or "unlink":" 1.2828 + if (command == "createlink" || command == "unlink") { 1.2829 + // "Let new parent be the result of calling createElement("a") on the 1.2830 + // ownerDocument of node." 1.2831 + newParent = node.ownerDocument.createElement("a"); 1.2832 + 1.2833 + // "Set the href attribute of new parent to new value." 1.2834 + newParent.setAttribute("href", newValue); 1.2835 + 1.2836 + // "Let ancestor be node's parent." 1.2837 + var ancestor = node.parentNode; 1.2838 + 1.2839 + // "While ancestor is not null:" 1.2840 + while (ancestor) { 1.2841 + // "If ancestor is an a, set the tag name of ancestor to "span", 1.2842 + // and let ancestor be the result." 1.2843 + if (isHtmlElement(ancestor, "A")) { 1.2844 + ancestor = setTagName(ancestor, "span"); 1.2845 + } 1.2846 + 1.2847 + // "Set ancestor to its parent." 1.2848 + ancestor = ancestor.parentNode; 1.2849 + } 1.2850 + } 1.2851 + 1.2852 + // "If command is "fontSize"; and new value is one of "x-small", "small", 1.2853 + // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the 1.2854 + // CSS styling flag is false, or new value is "xxx-large": let new parent 1.2855 + // be the result of calling createElement("font") on the ownerDocument of 1.2856 + // node, then set the size attribute of new parent to the number from the 1.2857 + // following table based on new value: [table omitted]" 1.2858 + if (command == "fontsize" 1.2859 + && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1 1.2860 + && (!cssStylingFlag || newValue == "xxx-large")) { 1.2861 + newParent = node.ownerDocument.createElement("font"); 1.2862 + newParent.size = cssSizeToLegacy(newValue); 1.2863 + } 1.2864 + 1.2865 + // "If command is "subscript" or "superscript" and new value is 1.2866 + // "subscript", let new parent be the result of calling 1.2867 + // createElement("sub") on the ownerDocument of node." 1.2868 + if ((command == "subscript" || command == "superscript") 1.2869 + && newValue == "subscript") { 1.2870 + newParent = node.ownerDocument.createElement("sub"); 1.2871 + } 1.2872 + 1.2873 + // "If command is "subscript" or "superscript" and new value is 1.2874 + // "superscript", let new parent be the result of calling 1.2875 + // createElement("sup") on the ownerDocument of node." 1.2876 + if ((command == "subscript" || command == "superscript") 1.2877 + && newValue == "superscript") { 1.2878 + newParent = node.ownerDocument.createElement("sup"); 1.2879 + } 1.2880 + 1.2881 + // "If new parent is null, let new parent be the result of calling 1.2882 + // createElement("span") on the ownerDocument of node." 1.2883 + if (!newParent) { 1.2884 + newParent = node.ownerDocument.createElement("span"); 1.2885 + } 1.2886 + 1.2887 + // "Insert new parent in node's parent before node." 1.2888 + node.parentNode.insertBefore(newParent, node); 1.2889 + 1.2890 + // "If the effective command value of command for new parent is not loosely 1.2891 + // equivalent to new value, and the relevant CSS property for command is 1.2892 + // not null, set that CSS property of new parent to new value (if the new 1.2893 + // value would be valid)." 1.2894 + var property = commands[command].relevantCssProperty; 1.2895 + if (property !== null 1.2896 + && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) { 1.2897 + newParent.style[property] = newValue; 1.2898 + } 1.2899 + 1.2900 + // "If command is "strikethrough", and new value is "line-through", and the 1.2901 + // effective command value of "strikethrough" for new parent is not 1.2902 + // "line-through", set the "text-decoration" property of new parent to 1.2903 + // "line-through"." 1.2904 + if (command == "strikethrough" 1.2905 + && newValue == "line-through" 1.2906 + && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { 1.2907 + newParent.style.textDecoration = "line-through"; 1.2908 + } 1.2909 + 1.2910 + // "If command is "underline", and new value is "underline", and the 1.2911 + // effective command value of "underline" for new parent is not 1.2912 + // "underline", set the "text-decoration" property of new parent to 1.2913 + // "underline"." 1.2914 + if (command == "underline" 1.2915 + && newValue == "underline" 1.2916 + && getEffectiveCommandValue(newParent, "underline") != "underline") { 1.2917 + newParent.style.textDecoration = "underline"; 1.2918 + } 1.2919 + 1.2920 + // "Append node to new parent as its last child, preserving ranges." 1.2921 + movePreservingRanges(node, newParent, newParent.childNodes.length); 1.2922 + 1.2923 + // "If node is an Element and the effective command value of command for 1.2924 + // node is not loosely equivalent to new value:" 1.2925 + if (node.nodeType == Node.ELEMENT_NODE 1.2926 + && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 1.2927 + // "Insert node into the parent of new parent before new parent, 1.2928 + // preserving ranges." 1.2929 + movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent)); 1.2930 + 1.2931 + // "Remove new parent from its parent." 1.2932 + newParent.parentNode.removeChild(newParent); 1.2933 + 1.2934 + // "Let children be all children of node, omitting any that are 1.2935 + // Elements whose specified command value for command is neither null 1.2936 + // nor equivalent to new value." 1.2937 + var children = []; 1.2938 + for (var i = 0; i < node.childNodes.length; i++) { 1.2939 + if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { 1.2940 + var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 1.2941 + 1.2942 + if (specifiedValue !== null 1.2943 + && !areEquivalentValues(command, newValue, specifiedValue)) { 1.2944 + continue; 1.2945 + } 1.2946 + } 1.2947 + children.push(node.childNodes[i]); 1.2948 + } 1.2949 + 1.2950 + // "Force the value of each Node in children, with command and new 1.2951 + // value as in this invocation of the algorithm." 1.2952 + for (var i = 0; i < children.length; i++) { 1.2953 + forceValue(children[i], command, newValue); 1.2954 + } 1.2955 + } 1.2956 +} 1.2957 + 1.2958 + 1.2959 +//@} 1.2960 +///// Setting the selection's value ///// 1.2961 +//@{ 1.2962 + 1.2963 +function setSelectionValue(command, newValue) { 1.2964 + // "If there is no formattable node effectively contained in the active 1.2965 + // range:" 1.2966 + if (!getAllEffectivelyContainedNodes(getActiveRange()) 1.2967 + .some(isFormattableNode)) { 1.2968 + // "If command has inline command activated values, set the state 1.2969 + // override to true if new value is among them and false if it's not." 1.2970 + if ("inlineCommandActivatedValues" in commands[command]) { 1.2971 + setStateOverride(command, commands[command].inlineCommandActivatedValues 1.2972 + .indexOf(newValue) != -1); 1.2973 + } 1.2974 + 1.2975 + // "If command is "subscript", unset the state override for 1.2976 + // "superscript"." 1.2977 + if (command == "subscript") { 1.2978 + unsetStateOverride("superscript"); 1.2979 + } 1.2980 + 1.2981 + // "If command is "superscript", unset the state override for 1.2982 + // "subscript"." 1.2983 + if (command == "superscript") { 1.2984 + unsetStateOverride("subscript"); 1.2985 + } 1.2986 + 1.2987 + // "If new value is null, unset the value override (if any)." 1.2988 + if (newValue === null) { 1.2989 + unsetValueOverride(command); 1.2990 + 1.2991 + // "Otherwise, if command is "createLink" or it has a value specified, 1.2992 + // set the value override to new value." 1.2993 + } else if (command == "createlink" || "value" in commands[command]) { 1.2994 + setValueOverride(command, newValue); 1.2995 + } 1.2996 + 1.2997 + // "Abort these steps." 1.2998 + return; 1.2999 + } 1.3000 + 1.3001 + // "If the active range's start node is an editable Text node, and its 1.3002 + // start offset is neither zero nor its start node's length, call 1.3003 + // splitText() on the active range's start node, with argument equal to the 1.3004 + // active range's start offset. Then set the active range's start node to 1.3005 + // the result, and its start offset to zero." 1.3006 + if (isEditable(getActiveRange().startContainer) 1.3007 + && getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.3008 + && getActiveRange().startOffset != 0 1.3009 + && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { 1.3010 + // Account for browsers not following range mutation rules 1.3011 + var newActiveRange = document.createRange(); 1.3012 + var newNode; 1.3013 + if (getActiveRange().startContainer == getActiveRange().endContainer) { 1.3014 + var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset; 1.3015 + newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); 1.3016 + newActiveRange.setEnd(newNode, newEndOffset); 1.3017 + getActiveRange().setEnd(newNode, newEndOffset); 1.3018 + } else { 1.3019 + newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); 1.3020 + } 1.3021 + newActiveRange.setStart(newNode, 0); 1.3022 + getSelection().removeAllRanges(); 1.3023 + getSelection().addRange(newActiveRange); 1.3024 + 1.3025 + getActiveRange().setStart(newNode, 0); 1.3026 + } 1.3027 + 1.3028 + // "If the active range's end node is an editable Text node, and its end 1.3029 + // offset is neither zero nor its end node's length, call splitText() on 1.3030 + // the active range's end node, with argument equal to the active range's 1.3031 + // end offset." 1.3032 + if (isEditable(getActiveRange().endContainer) 1.3033 + && getActiveRange().endContainer.nodeType == Node.TEXT_NODE 1.3034 + && getActiveRange().endOffset != 0 1.3035 + && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { 1.3036 + // IE seems to mutate the range incorrectly here, so we need correction 1.3037 + // here as well. The active range will be temporarily in orphaned 1.3038 + // nodes, so calling getActiveRange() after splitText() but before 1.3039 + // fixing the range will throw an exception. 1.3040 + var activeRange = getActiveRange(); 1.3041 + var newStart = [activeRange.startContainer, activeRange.startOffset]; 1.3042 + var newEnd = [activeRange.endContainer, activeRange.endOffset]; 1.3043 + activeRange.endContainer.splitText(activeRange.endOffset); 1.3044 + activeRange.setStart(newStart[0], newStart[1]); 1.3045 + activeRange.setEnd(newEnd[0], newEnd[1]); 1.3046 + 1.3047 + getSelection().removeAllRanges(); 1.3048 + getSelection().addRange(activeRange); 1.3049 + } 1.3050 + 1.3051 + // "Let element list be all editable Elements effectively contained in the 1.3052 + // active range. 1.3053 + // 1.3054 + // "For each element in element list, clear the value of element." 1.3055 + getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 1.3056 + return isEditable(node) && node.nodeType == Node.ELEMENT_NODE; 1.3057 + }).forEach(function(element) { 1.3058 + clearValue(element, command); 1.3059 + }); 1.3060 + 1.3061 + // "Let node list be all editable nodes effectively contained in the active 1.3062 + // range. 1.3063 + // 1.3064 + // "For each node in node list:" 1.3065 + getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { 1.3066 + // "Push down values on node." 1.3067 + pushDownValues(node, command, newValue); 1.3068 + 1.3069 + // "If node is an allowed child of span, force the value of node." 1.3070 + if (isAllowedChild(node, "span")) { 1.3071 + forceValue(node, command, newValue); 1.3072 + } 1.3073 + }); 1.3074 +} 1.3075 + 1.3076 + 1.3077 +//@} 1.3078 +///// The backColor command ///// 1.3079 +//@{ 1.3080 +commands.backcolor = { 1.3081 + // Copy-pasted, same as hiliteColor 1.3082 + action: function(value) { 1.3083 + // Action is further copy-pasted, same as foreColor 1.3084 + 1.3085 + // "If value is not a valid CSS color, prepend "#" to it." 1.3086 + // 1.3087 + // "If value is still not a valid CSS color, or if it is currentColor, 1.3088 + // return false." 1.3089 + // 1.3090 + // Cheap hack for testing, no attempt to be comprehensive. 1.3091 + if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 1.3092 + value = "#" + value; 1.3093 + } 1.3094 + if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 1.3095 + && !parseSimpleColor(value) 1.3096 + && value.toLowerCase() != "transparent") { 1.3097 + return false; 1.3098 + } 1.3099 + 1.3100 + // "Set the selection's value to value." 1.3101 + setSelectionValue("backcolor", value); 1.3102 + 1.3103 + // "Return true." 1.3104 + return true; 1.3105 + }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", 1.3106 + equivalentValues: function(val1, val2) { 1.3107 + // "Either both strings are valid CSS colors and have the same red, 1.3108 + // green, blue, and alpha components, or neither string is a valid CSS 1.3109 + // color." 1.3110 + return normalizeColor(val1) === normalizeColor(val2); 1.3111 + }, 1.3112 +}; 1.3113 + 1.3114 +//@} 1.3115 +///// The bold command ///// 1.3116 +//@{ 1.3117 +commands.bold = { 1.3118 + action: function() { 1.3119 + // "If queryCommandState("bold") returns true, set the selection's 1.3120 + // value to "normal". Otherwise set the selection's value to "bold". 1.3121 + // Either way, return true." 1.3122 + if (myQueryCommandState("bold")) { 1.3123 + setSelectionValue("bold", "normal"); 1.3124 + } else { 1.3125 + setSelectionValue("bold", "bold"); 1.3126 + } 1.3127 + return true; 1.3128 + }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], 1.3129 + relevantCssProperty: "fontWeight", 1.3130 + equivalentValues: function(val1, val2) { 1.3131 + // "Either the two strings are equal, or one is "bold" and the other is 1.3132 + // "700", or one is "normal" and the other is "400"." 1.3133 + return val1 == val2 1.3134 + || (val1 == "bold" && val2 == "700") 1.3135 + || (val1 == "700" && val2 == "bold") 1.3136 + || (val1 == "normal" && val2 == "400") 1.3137 + || (val1 == "400" && val2 == "normal"); 1.3138 + }, 1.3139 +}; 1.3140 + 1.3141 +//@} 1.3142 +///// The createLink command ///// 1.3143 +//@{ 1.3144 +commands.createlink = { 1.3145 + action: function(value) { 1.3146 + // "If value is the empty string, return false." 1.3147 + if (value === "") { 1.3148 + return false; 1.3149 + } 1.3150 + 1.3151 + // "For each editable a element that has an href attribute and is an 1.3152 + // ancestor of some node effectively contained in the active range, set 1.3153 + // that a element's href attribute to value." 1.3154 + // 1.3155 + // TODO: We don't actually do this in tree order, not that it matters 1.3156 + // unless you're spying with mutation events. 1.3157 + getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) { 1.3158 + getAncestors(node).forEach(function(ancestor) { 1.3159 + if (isEditable(ancestor) 1.3160 + && isHtmlElement(ancestor, "a") 1.3161 + && ancestor.hasAttribute("href")) { 1.3162 + ancestor.setAttribute("href", value); 1.3163 + } 1.3164 + }); 1.3165 + }); 1.3166 + 1.3167 + // "Set the selection's value to value." 1.3168 + setSelectionValue("createlink", value); 1.3169 + 1.3170 + // "Return true." 1.3171 + return true; 1.3172 + } 1.3173 +}; 1.3174 + 1.3175 +//@} 1.3176 +///// The fontName command ///// 1.3177 +//@{ 1.3178 +commands.fontname = { 1.3179 + action: function(value) { 1.3180 + // "Set the selection's value to value, then return true." 1.3181 + setSelectionValue("fontname", value); 1.3182 + return true; 1.3183 + }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily" 1.3184 +}; 1.3185 + 1.3186 +//@} 1.3187 +///// The fontSize command ///// 1.3188 +//@{ 1.3189 + 1.3190 +// Helper function for fontSize's action plus queryOutputHelper. It's just the 1.3191 +// middle of fontSize's action, ripped out into its own function. Returns null 1.3192 +// if the size is invalid. 1.3193 +function normalizeFontSize(value) { 1.3194 + // "Strip leading and trailing whitespace from value." 1.3195 + // 1.3196 + // Cheap hack, not following the actual algorithm. 1.3197 + value = value.trim(); 1.3198 + 1.3199 + // "If value is not a valid floating point number, and would not be a valid 1.3200 + // floating point number if a single leading "+" character were stripped, 1.3201 + // return false." 1.3202 + if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) { 1.3203 + return null; 1.3204 + } 1.3205 + 1.3206 + var mode; 1.3207 + 1.3208 + // "If the first character of value is "+", delete the character and let 1.3209 + // mode be "relative-plus"." 1.3210 + if (value[0] == "+") { 1.3211 + value = value.slice(1); 1.3212 + mode = "relative-plus"; 1.3213 + // "Otherwise, if the first character of value is "-", delete the character 1.3214 + // and let mode be "relative-minus"." 1.3215 + } else if (value[0] == "-") { 1.3216 + value = value.slice(1); 1.3217 + mode = "relative-minus"; 1.3218 + // "Otherwise, let mode be "absolute"." 1.3219 + } else { 1.3220 + mode = "absolute"; 1.3221 + } 1.3222 + 1.3223 + // "Apply the rules for parsing non-negative integers to value, and let 1.3224 + // number be the result." 1.3225 + // 1.3226 + // Another cheap hack. 1.3227 + var num = parseInt(value); 1.3228 + 1.3229 + // "If mode is "relative-plus", add three to number." 1.3230 + if (mode == "relative-plus") { 1.3231 + num += 3; 1.3232 + } 1.3233 + 1.3234 + // "If mode is "relative-minus", negate number, then add three to it." 1.3235 + if (mode == "relative-minus") { 1.3236 + num = 3 - num; 1.3237 + } 1.3238 + 1.3239 + // "If number is less than one, let number equal 1." 1.3240 + if (num < 1) { 1.3241 + num = 1; 1.3242 + } 1.3243 + 1.3244 + // "If number is greater than seven, let number equal 7." 1.3245 + if (num > 7) { 1.3246 + num = 7; 1.3247 + } 1.3248 + 1.3249 + // "Set value to the string here corresponding to number:" [table omitted] 1.3250 + value = { 1.3251 + 1: "x-small", 1.3252 + 2: "small", 1.3253 + 3: "medium", 1.3254 + 4: "large", 1.3255 + 5: "x-large", 1.3256 + 6: "xx-large", 1.3257 + 7: "xxx-large" 1.3258 + }[num]; 1.3259 + 1.3260 + return value; 1.3261 +} 1.3262 + 1.3263 +commands.fontsize = { 1.3264 + action: function(value) { 1.3265 + value = normalizeFontSize(value); 1.3266 + if (value === null) { 1.3267 + return false; 1.3268 + } 1.3269 + 1.3270 + // "Set the selection's value to value." 1.3271 + setSelectionValue("fontsize", value); 1.3272 + 1.3273 + // "Return true." 1.3274 + return true; 1.3275 + }, indeterm: function() { 1.3276 + // "True if among formattable nodes that are effectively contained in 1.3277 + // the active range, there are two that have distinct effective command 1.3278 + // values. Otherwise false." 1.3279 + return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) 1.3280 + .map(function(node) { 1.3281 + return getEffectiveCommandValue(node, "fontsize"); 1.3282 + }).filter(function(value, i, arr) { 1.3283 + return arr.slice(0, i).indexOf(value) == -1; 1.3284 + }).length >= 2; 1.3285 + }, value: function() { 1.3286 + // "If the active range is null, return the empty string." 1.3287 + if (!getActiveRange()) { 1.3288 + return ""; 1.3289 + } 1.3290 + 1.3291 + // "Let pixel size be the effective command value of the first 1.3292 + // formattable node that is effectively contained in the active range, 1.3293 + // or if there is no such node, the effective command value of the 1.3294 + // active range's start node, in either case interpreted as a number of 1.3295 + // pixels." 1.3296 + var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; 1.3297 + if (node === undefined) { 1.3298 + node = getActiveRange().startContainer; 1.3299 + } 1.3300 + var pixelSize = getEffectiveCommandValue(node, "fontsize"); 1.3301 + 1.3302 + // "Return the legacy font size for pixel size." 1.3303 + return getLegacyFontSize(pixelSize); 1.3304 + }, relevantCssProperty: "fontSize" 1.3305 +}; 1.3306 + 1.3307 +function getLegacyFontSize(size) { 1.3308 + if (getLegacyFontSize.resultCache === undefined) { 1.3309 + getLegacyFontSize.resultCache = {}; 1.3310 + } 1.3311 + 1.3312 + if (getLegacyFontSize.resultCache[size] !== undefined) { 1.3313 + return getLegacyFontSize.resultCache[size]; 1.3314 + } 1.3315 + 1.3316 + // For convenience in other places in my code, I handle all sizes, not just 1.3317 + // pixel sizes as the spec says. This means pixel sizes have to be passed 1.3318 + // in suffixed with "px", not as plain numbers. 1.3319 + if (normalizeFontSize(size) !== null) { 1.3320 + return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size)); 1.3321 + } 1.3322 + 1.3323 + if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1 1.3324 + && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { 1.3325 + // There is no sensible legacy size for things like "2em". 1.3326 + return getLegacyFontSize.resultCache[size] = null; 1.3327 + } 1.3328 + 1.3329 + var font = document.createElement("font"); 1.3330 + document.body.appendChild(font); 1.3331 + if (size == "xxx-large") { 1.3332 + font.size = 7; 1.3333 + } else { 1.3334 + font.style.fontSize = size; 1.3335 + } 1.3336 + var pixelSize = parseInt(getComputedStyle(font).fontSize); 1.3337 + document.body.removeChild(font); 1.3338 + 1.3339 + // "Let returned size be 1." 1.3340 + var returnedSize = 1; 1.3341 + 1.3342 + // "While returned size is less than 7:" 1.3343 + while (returnedSize < 7) { 1.3344 + // "Let lower bound be the resolved value of "font-size" in pixels 1.3345 + // of a font element whose size attribute is set to returned size." 1.3346 + var font = document.createElement("font"); 1.3347 + font.size = returnedSize; 1.3348 + document.body.appendChild(font); 1.3349 + var lowerBound = parseInt(getComputedStyle(font).fontSize); 1.3350 + 1.3351 + // "Let upper bound be the resolved value of "font-size" in pixels 1.3352 + // of a font element whose size attribute is set to one plus 1.3353 + // returned size." 1.3354 + font.size = 1 + returnedSize; 1.3355 + var upperBound = parseInt(getComputedStyle(font).fontSize); 1.3356 + document.body.removeChild(font); 1.3357 + 1.3358 + // "Let average be the average of upper bound and lower bound." 1.3359 + var average = (upperBound + lowerBound)/2; 1.3360 + 1.3361 + // "If pixel size is less than average, return the one-element 1.3362 + // string consisting of the digit returned size." 1.3363 + if (pixelSize < average) { 1.3364 + return getLegacyFontSize.resultCache[size] = String(returnedSize); 1.3365 + } 1.3366 + 1.3367 + // "Add one to returned size." 1.3368 + returnedSize++; 1.3369 + } 1.3370 + 1.3371 + // "Return "7"." 1.3372 + return getLegacyFontSize.resultCache[size] = "7"; 1.3373 +} 1.3374 + 1.3375 +//@} 1.3376 +///// The foreColor command ///// 1.3377 +//@{ 1.3378 +commands.forecolor = { 1.3379 + action: function(value) { 1.3380 + // Copy-pasted, same as backColor and hiliteColor 1.3381 + 1.3382 + // "If value is not a valid CSS color, prepend "#" to it." 1.3383 + // 1.3384 + // "If value is still not a valid CSS color, or if it is currentColor, 1.3385 + // return false." 1.3386 + // 1.3387 + // Cheap hack for testing, no attempt to be comprehensive. 1.3388 + if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 1.3389 + value = "#" + value; 1.3390 + } 1.3391 + if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 1.3392 + && !parseSimpleColor(value) 1.3393 + && value.toLowerCase() != "transparent") { 1.3394 + return false; 1.3395 + } 1.3396 + 1.3397 + // "Set the selection's value to value." 1.3398 + setSelectionValue("forecolor", value); 1.3399 + 1.3400 + // "Return true." 1.3401 + return true; 1.3402 + }, standardInlineValueCommand: true, relevantCssProperty: "color", 1.3403 + equivalentValues: function(val1, val2) { 1.3404 + // "Either both strings are valid CSS colors and have the same red, 1.3405 + // green, blue, and alpha components, or neither string is a valid CSS 1.3406 + // color." 1.3407 + return normalizeColor(val1) === normalizeColor(val2); 1.3408 + }, 1.3409 +}; 1.3410 + 1.3411 +//@} 1.3412 +///// The hiliteColor command ///// 1.3413 +//@{ 1.3414 +commands.hilitecolor = { 1.3415 + // Copy-pasted, same as backColor 1.3416 + action: function(value) { 1.3417 + // Action is further copy-pasted, same as foreColor 1.3418 + 1.3419 + // "If value is not a valid CSS color, prepend "#" to it." 1.3420 + // 1.3421 + // "If value is still not a valid CSS color, or if it is currentColor, 1.3422 + // return false." 1.3423 + // 1.3424 + // Cheap hack for testing, no attempt to be comprehensive. 1.3425 + if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 1.3426 + value = "#" + value; 1.3427 + } 1.3428 + if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 1.3429 + && !parseSimpleColor(value) 1.3430 + && value.toLowerCase() != "transparent") { 1.3431 + return false; 1.3432 + } 1.3433 + 1.3434 + // "Set the selection's value to value." 1.3435 + setSelectionValue("hilitecolor", value); 1.3436 + 1.3437 + // "Return true." 1.3438 + return true; 1.3439 + }, indeterm: function() { 1.3440 + // "True if among editable Text nodes that are effectively contained in 1.3441 + // the active range, there are two that have distinct effective command 1.3442 + // values. Otherwise false." 1.3443 + return getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 1.3444 + return isEditable(node) && node.nodeType == Node.TEXT_NODE; 1.3445 + }).map(function(node) { 1.3446 + return getEffectiveCommandValue(node, "hilitecolor"); 1.3447 + }).filter(function(value, i, arr) { 1.3448 + return arr.slice(0, i).indexOf(value) == -1; 1.3449 + }).length >= 2; 1.3450 + }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", 1.3451 + equivalentValues: function(val1, val2) { 1.3452 + // "Either both strings are valid CSS colors and have the same red, 1.3453 + // green, blue, and alpha components, or neither string is a valid CSS 1.3454 + // color." 1.3455 + return normalizeColor(val1) === normalizeColor(val2); 1.3456 + }, 1.3457 +}; 1.3458 + 1.3459 +//@} 1.3460 +///// The italic command ///// 1.3461 +//@{ 1.3462 +commands.italic = { 1.3463 + action: function() { 1.3464 + // "If queryCommandState("italic") returns true, set the selection's 1.3465 + // value to "normal". Otherwise set the selection's value to "italic". 1.3466 + // Either way, return true." 1.3467 + if (myQueryCommandState("italic")) { 1.3468 + setSelectionValue("italic", "normal"); 1.3469 + } else { 1.3470 + setSelectionValue("italic", "italic"); 1.3471 + } 1.3472 + return true; 1.3473 + }, inlineCommandActivatedValues: ["italic", "oblique"], 1.3474 + relevantCssProperty: "fontStyle" 1.3475 +}; 1.3476 + 1.3477 +//@} 1.3478 +///// The removeFormat command ///// 1.3479 +//@{ 1.3480 +commands.removeformat = { 1.3481 + action: function() { 1.3482 + // "A removeFormat candidate is an editable HTML element with local 1.3483 + // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", 1.3484 + // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", 1.3485 + // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", 1.3486 + // "tt", "u", or "var"." 1.3487 + function isRemoveFormatCandidate(node) { 1.3488 + return isEditable(node) 1.3489 + && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo", 1.3490 + "big", "blink", "cite", "code", "dfn", "em", "font", "i", 1.3491 + "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", 1.3492 + "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); 1.3493 + } 1.3494 + 1.3495 + // "Let elements to remove be a list of every removeFormat candidate 1.3496 + // effectively contained in the active range." 1.3497 + var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate); 1.3498 + 1.3499 + // "For each element in elements to remove:" 1.3500 + elementsToRemove.forEach(function(element) { 1.3501 + // "While element has children, insert the first child of element 1.3502 + // into the parent of element immediately before element, 1.3503 + // preserving ranges." 1.3504 + while (element.hasChildNodes()) { 1.3505 + movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element)); 1.3506 + } 1.3507 + 1.3508 + // "Remove element from its parent." 1.3509 + element.parentNode.removeChild(element); 1.3510 + }); 1.3511 + 1.3512 + // "If the active range's start node is an editable Text node, and its 1.3513 + // start offset is neither zero nor its start node's length, call 1.3514 + // splitText() on the active range's start node, with argument equal to 1.3515 + // the active range's start offset. Then set the active range's start 1.3516 + // node to the result, and its start offset to zero." 1.3517 + if (isEditable(getActiveRange().startContainer) 1.3518 + && getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.3519 + && getActiveRange().startOffset != 0 1.3520 + && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { 1.3521 + // Account for browsers not following range mutation rules 1.3522 + if (getActiveRange().startContainer == getActiveRange().endContainer) { 1.3523 + var newEnd = getActiveRange().endOffset - getActiveRange().startOffset; 1.3524 + var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); 1.3525 + getActiveRange().setStart(newNode, 0); 1.3526 + getActiveRange().setEnd(newNode, newEnd); 1.3527 + } else { 1.3528 + getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0); 1.3529 + } 1.3530 + } 1.3531 + 1.3532 + // "If the active range's end node is an editable Text node, and its 1.3533 + // end offset is neither zero nor its end node's length, call 1.3534 + // splitText() on the active range's end node, with argument equal to 1.3535 + // the active range's end offset." 1.3536 + if (isEditable(getActiveRange().endContainer) 1.3537 + && getActiveRange().endContainer.nodeType == Node.TEXT_NODE 1.3538 + && getActiveRange().endOffset != 0 1.3539 + && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { 1.3540 + // IE seems to mutate the range incorrectly here, so we need 1.3541 + // correction here as well. Have to be careful to set the range to 1.3542 + // something not including the text node so that getActiveRange() 1.3543 + // doesn't throw an exception due to a temporarily detached 1.3544 + // endpoint. 1.3545 + var newStart = [getActiveRange().startContainer, getActiveRange().startOffset]; 1.3546 + var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset]; 1.3547 + getActiveRange().setEnd(document.documentElement, 0); 1.3548 + newEnd[0].splitText(newEnd[1]); 1.3549 + getActiveRange().setStart(newStart[0], newStart[1]); 1.3550 + getActiveRange().setEnd(newEnd[0], newEnd[1]); 1.3551 + } 1.3552 + 1.3553 + // "Let node list consist of all editable nodes effectively contained 1.3554 + // in the active range." 1.3555 + // 1.3556 + // "For each node in node list, while node's parent is a removeFormat 1.3557 + // candidate in the same editing host as node, split the parent of the 1.3558 + // one-node list consisting of node." 1.3559 + getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { 1.3560 + while (isRemoveFormatCandidate(node.parentNode) 1.3561 + && inSameEditingHost(node.parentNode, node)) { 1.3562 + splitParent([node]); 1.3563 + } 1.3564 + }); 1.3565 + 1.3566 + // "For each of the entries in the following list, in the given order, 1.3567 + // set the selection's value to null, with command as given." 1.3568 + [ 1.3569 + "subscript", 1.3570 + "bold", 1.3571 + "fontname", 1.3572 + "fontsize", 1.3573 + "forecolor", 1.3574 + "hilitecolor", 1.3575 + "italic", 1.3576 + "strikethrough", 1.3577 + "underline", 1.3578 + ].forEach(function(command) { 1.3579 + setSelectionValue(command, null); 1.3580 + }); 1.3581 + 1.3582 + // "Return true." 1.3583 + return true; 1.3584 + } 1.3585 +}; 1.3586 + 1.3587 +//@} 1.3588 +///// The strikethrough command ///// 1.3589 +//@{ 1.3590 +commands.strikethrough = { 1.3591 + action: function() { 1.3592 + // "If queryCommandState("strikethrough") returns true, set the 1.3593 + // selection's value to null. Otherwise set the selection's value to 1.3594 + // "line-through". Either way, return true." 1.3595 + if (myQueryCommandState("strikethrough")) { 1.3596 + setSelectionValue("strikethrough", null); 1.3597 + } else { 1.3598 + setSelectionValue("strikethrough", "line-through"); 1.3599 + } 1.3600 + return true; 1.3601 + }, inlineCommandActivatedValues: ["line-through"] 1.3602 +}; 1.3603 + 1.3604 +//@} 1.3605 +///// The subscript command ///// 1.3606 +//@{ 1.3607 +commands.subscript = { 1.3608 + action: function() { 1.3609 + // "Call queryCommandState("subscript"), and let state be the result." 1.3610 + var state = myQueryCommandState("subscript"); 1.3611 + 1.3612 + // "Set the selection's value to null." 1.3613 + setSelectionValue("subscript", null); 1.3614 + 1.3615 + // "If state is false, set the selection's value to "subscript"." 1.3616 + if (!state) { 1.3617 + setSelectionValue("subscript", "subscript"); 1.3618 + } 1.3619 + 1.3620 + // "Return true." 1.3621 + return true; 1.3622 + }, indeterm: function() { 1.3623 + // "True if either among formattable nodes that are effectively 1.3624 + // contained in the active range, there is at least one with effective 1.3625 + // command value "subscript" and at least one with some other effective 1.3626 + // command value; or if there is some formattable node effectively 1.3627 + // contained in the active range with effective command value "mixed". 1.3628 + // Otherwise false." 1.3629 + var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); 1.3630 + return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" }) 1.3631 + && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" })) 1.3632 + || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" }); 1.3633 + }, inlineCommandActivatedValues: ["subscript"], 1.3634 +}; 1.3635 + 1.3636 +//@} 1.3637 +///// The superscript command ///// 1.3638 +//@{ 1.3639 +commands.superscript = { 1.3640 + action: function() { 1.3641 + // "Call queryCommandState("superscript"), and let state be the 1.3642 + // result." 1.3643 + var state = myQueryCommandState("superscript"); 1.3644 + 1.3645 + // "Set the selection's value to null." 1.3646 + setSelectionValue("superscript", null); 1.3647 + 1.3648 + // "If state is false, set the selection's value to "superscript"." 1.3649 + if (!state) { 1.3650 + setSelectionValue("superscript", "superscript"); 1.3651 + } 1.3652 + 1.3653 + // "Return true." 1.3654 + return true; 1.3655 + }, indeterm: function() { 1.3656 + // "True if either among formattable nodes that are effectively 1.3657 + // contained in the active range, there is at least one with effective 1.3658 + // command value "superscript" and at least one with some other 1.3659 + // effective command value; or if there is some formattable node 1.3660 + // effectively contained in the active range with effective command 1.3661 + // value "mixed". Otherwise false." 1.3662 + var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); 1.3663 + return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" }) 1.3664 + && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" })) 1.3665 + || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" }); 1.3666 + }, inlineCommandActivatedValues: ["superscript"], 1.3667 +}; 1.3668 + 1.3669 +//@} 1.3670 +///// The underline command ///// 1.3671 +//@{ 1.3672 +commands.underline = { 1.3673 + action: function() { 1.3674 + // "If queryCommandState("underline") returns true, set the selection's 1.3675 + // value to null. Otherwise set the selection's value to "underline". 1.3676 + // Either way, return true." 1.3677 + if (myQueryCommandState("underline")) { 1.3678 + setSelectionValue("underline", null); 1.3679 + } else { 1.3680 + setSelectionValue("underline", "underline"); 1.3681 + } 1.3682 + return true; 1.3683 + }, inlineCommandActivatedValues: ["underline"] 1.3684 +}; 1.3685 + 1.3686 +//@} 1.3687 +///// The unlink command ///// 1.3688 +//@{ 1.3689 +commands.unlink = { 1.3690 + action: function() { 1.3691 + // "Let hyperlinks be a list of every a element that has an href 1.3692 + // attribute and is contained in the active range or is an ancestor of 1.3693 + // one of its boundary points." 1.3694 + // 1.3695 + // As usual, take care to ensure it's tree order. The correctness of 1.3696 + // the following is left as an exercise for the reader. 1.3697 + var range = getActiveRange(); 1.3698 + var hyperlinks = []; 1.3699 + for ( 1.3700 + var node = range.startContainer; 1.3701 + node; 1.3702 + node = node.parentNode 1.3703 + ) { 1.3704 + if (isHtmlElement(node, "A") 1.3705 + && node.hasAttribute("href")) { 1.3706 + hyperlinks.unshift(node); 1.3707 + } 1.3708 + } 1.3709 + for ( 1.3710 + var node = range.startContainer; 1.3711 + node != nextNodeDescendants(range.endContainer); 1.3712 + node = nextNode(node) 1.3713 + ) { 1.3714 + if (isHtmlElement(node, "A") 1.3715 + && node.hasAttribute("href") 1.3716 + && (isContained(node, range) 1.3717 + || isAncestor(node, range.endContainer) 1.3718 + || node == range.endContainer)) { 1.3719 + hyperlinks.push(node); 1.3720 + } 1.3721 + } 1.3722 + 1.3723 + // "Clear the value of each member of hyperlinks." 1.3724 + for (var i = 0; i < hyperlinks.length; i++) { 1.3725 + clearValue(hyperlinks[i], "unlink"); 1.3726 + } 1.3727 + 1.3728 + // "Return true." 1.3729 + return true; 1.3730 + } 1.3731 +}; 1.3732 + 1.3733 +//@} 1.3734 + 1.3735 +///////////////////////////////////// 1.3736 +///// Block formatting commands ///// 1.3737 +///////////////////////////////////// 1.3738 + 1.3739 +///// Block formatting command definitions ///// 1.3740 +//@{ 1.3741 + 1.3742 +// "An indentation element is either a blockquote, or a div that has a style 1.3743 +// attribute that sets "margin" or some subproperty of it." 1.3744 +function isIndentationElement(node) { 1.3745 + if (!isHtmlElement(node)) { 1.3746 + return false; 1.3747 + } 1.3748 + 1.3749 + if (node.tagName == "BLOCKQUOTE") { 1.3750 + return true; 1.3751 + } 1.3752 + 1.3753 + if (node.tagName != "DIV") { 1.3754 + return false; 1.3755 + } 1.3756 + 1.3757 + for (var i = 0; i < node.style.length; i++) { 1.3758 + // Approximate check 1.3759 + if (/^(-[a-z]+-)?margin/.test(node.style[i])) { 1.3760 + return true; 1.3761 + } 1.3762 + } 1.3763 + 1.3764 + return false; 1.3765 +} 1.3766 + 1.3767 +// "A simple indentation element is an indentation element that has no 1.3768 +// attributes except possibly 1.3769 +// 1.3770 +// * "a style attribute that sets no properties other than "margin", 1.3771 +// "border", "padding", or subproperties of those; and/or 1.3772 +// * "a dir attribute." 1.3773 +function isSimpleIndentationElement(node) { 1.3774 + if (!isIndentationElement(node)) { 1.3775 + return false; 1.3776 + } 1.3777 + 1.3778 + for (var i = 0; i < node.attributes.length; i++) { 1.3779 + if (!isHtmlNamespace(node.attributes[i].namespaceURI) 1.3780 + || ["style", "dir"].indexOf(node.attributes[i].name) == -1) { 1.3781 + return false; 1.3782 + } 1.3783 + } 1.3784 + 1.3785 + for (var i = 0; i < node.style.length; i++) { 1.3786 + // This is approximate, but it works well enough for my purposes. 1.3787 + if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { 1.3788 + return false; 1.3789 + } 1.3790 + } 1.3791 + 1.3792 + return true; 1.3793 +} 1.3794 + 1.3795 +// "A non-list single-line container is an HTML element with local name 1.3796 +// "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", 1.3797 +// or "xmp"." 1.3798 +function isNonListSingleLineContainer(node) { 1.3799 + return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", 1.3800 + "h6", "listing", "p", "pre", "xmp"]); 1.3801 +} 1.3802 + 1.3803 +// "A single-line container is either a non-list single-line container, or an 1.3804 +// HTML element with local name "li", "dt", or "dd"." 1.3805 +function isSingleLineContainer(node) { 1.3806 + return isNonListSingleLineContainer(node) 1.3807 + || isHtmlElement(node, ["li", "dt", "dd"]); 1.3808 +} 1.3809 + 1.3810 +function getBlockNodeOf(node) { 1.3811 + // "While node is an inline node, set node to its parent." 1.3812 + while (isInlineNode(node)) { 1.3813 + node = node.parentNode; 1.3814 + } 1.3815 + 1.3816 + // "Return node." 1.3817 + return node; 1.3818 +} 1.3819 + 1.3820 +//@} 1.3821 +///// Assorted block formatting command algorithms ///// 1.3822 +//@{ 1.3823 + 1.3824 +function fixDisallowedAncestors(node) { 1.3825 + // "If node is not editable, abort these steps." 1.3826 + if (!isEditable(node)) { 1.3827 + return; 1.3828 + } 1.3829 + 1.3830 + // "If node is not an allowed child of any of its ancestors in the same 1.3831 + // editing host:" 1.3832 + if (getAncestors(node).every(function(ancestor) { 1.3833 + return !inSameEditingHost(node, ancestor) 1.3834 + || !isAllowedChild(node, ancestor) 1.3835 + })) { 1.3836 + // "If node is a dd or dt, wrap the one-node list consisting of node, 1.3837 + // with sibling criteria returning true for any dl with no attributes 1.3838 + // and false otherwise, and new parent instructions returning the 1.3839 + // result of calling createElement("dl") on the context object. Then 1.3840 + // abort these steps." 1.3841 + if (isHtmlElement(node, ["dd", "dt"])) { 1.3842 + wrap([node], 1.3843 + function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length }, 1.3844 + function() { return document.createElement("dl") }); 1.3845 + return; 1.3846 + } 1.3847 + 1.3848 + // "If "p" is not an allowed child of the editing host of node, abort 1.3849 + // these steps." 1.3850 + if (!isAllowedChild("p", getEditingHostOf(node))) { 1.3851 + return; 1.3852 + } 1.3853 + 1.3854 + // "If node is not a prohibited paragraph child, abort these steps." 1.3855 + if (!isProhibitedParagraphChild(node)) { 1.3856 + return; 1.3857 + } 1.3858 + 1.3859 + // "Set the tag name of node to the default single-line container name, 1.3860 + // and let node be the result." 1.3861 + node = setTagName(node, defaultSingleLineContainerName); 1.3862 + 1.3863 + // "Fix disallowed ancestors of node." 1.3864 + fixDisallowedAncestors(node); 1.3865 + 1.3866 + // "Let children be node's children." 1.3867 + var children = [].slice.call(node.childNodes); 1.3868 + 1.3869 + // "For each child in children, if child is a prohibited paragraph 1.3870 + // child:" 1.3871 + children.filter(isProhibitedParagraphChild) 1.3872 + .forEach(function(child) { 1.3873 + // "Record the values of the one-node list consisting of child, and 1.3874 + // let values be the result." 1.3875 + var values = recordValues([child]); 1.3876 + 1.3877 + // "Split the parent of the one-node list consisting of child." 1.3878 + splitParent([child]); 1.3879 + 1.3880 + // "Restore the values from values." 1.3881 + restoreValues(values); 1.3882 + }); 1.3883 + 1.3884 + // "Abort these steps." 1.3885 + return; 1.3886 + } 1.3887 + 1.3888 + // "Record the values of the one-node list consisting of node, and let 1.3889 + // values be the result." 1.3890 + var values = recordValues([node]); 1.3891 + 1.3892 + // "While node is not an allowed child of its parent, split the parent of 1.3893 + // the one-node list consisting of node." 1.3894 + while (!isAllowedChild(node, node.parentNode)) { 1.3895 + splitParent([node]); 1.3896 + } 1.3897 + 1.3898 + // "Restore the values from values." 1.3899 + restoreValues(values); 1.3900 +} 1.3901 + 1.3902 +function normalizeSublists(item) { 1.3903 + // "If item is not an li or it is not editable or its parent is not 1.3904 + // editable, abort these steps." 1.3905 + if (!isHtmlElement(item, "LI") 1.3906 + || !isEditable(item) 1.3907 + || !isEditable(item.parentNode)) { 1.3908 + return; 1.3909 + } 1.3910 + 1.3911 + // "Let new item be null." 1.3912 + var newItem = null; 1.3913 + 1.3914 + // "While item has an ol or ul child:" 1.3915 + while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) { 1.3916 + // "Let child be the last child of item." 1.3917 + var child = item.lastChild; 1.3918 + 1.3919 + // "If child is an ol or ul, or new item is null and child is a Text 1.3920 + // node whose data consists of zero of more space characters:" 1.3921 + if (isHtmlElement(child, ["OL", "UL"]) 1.3922 + || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) { 1.3923 + // "Set new item to null." 1.3924 + newItem = null; 1.3925 + 1.3926 + // "Insert child into the parent of item immediately following 1.3927 + // item, preserving ranges." 1.3928 + movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item)); 1.3929 + 1.3930 + // "Otherwise:" 1.3931 + } else { 1.3932 + // "If new item is null, let new item be the result of calling 1.3933 + // createElement("li") on the ownerDocument of item, then insert 1.3934 + // new item into the parent of item immediately after item." 1.3935 + if (!newItem) { 1.3936 + newItem = item.ownerDocument.createElement("li"); 1.3937 + item.parentNode.insertBefore(newItem, item.nextSibling); 1.3938 + } 1.3939 + 1.3940 + // "Insert child into new item as its first child, preserving 1.3941 + // ranges." 1.3942 + movePreservingRanges(child, newItem, 0); 1.3943 + } 1.3944 + } 1.3945 +} 1.3946 + 1.3947 +function getSelectionListState() { 1.3948 + // "If the active range is null, return "none"." 1.3949 + if (!getActiveRange()) { 1.3950 + return "none"; 1.3951 + } 1.3952 + 1.3953 + // "Block-extend the active range, and let new range be the result." 1.3954 + var newRange = blockExtend(getActiveRange()); 1.3955 + 1.3956 + // "Let node list be a list of nodes, initially empty." 1.3957 + // 1.3958 + // "For each node contained in new range, append node to node list if the 1.3959 + // last member of node list (if any) is not an ancestor of node; node is 1.3960 + // editable; node is not an indentation element; and node is either an ol 1.3961 + // or ul, or the child of an ol or ul, or an allowed child of "li"." 1.3962 + var nodeList = getContainedNodes(newRange, function(node) { 1.3963 + return isEditable(node) 1.3964 + && !isIndentationElement(node) 1.3965 + && (isHtmlElement(node, ["ol", "ul"]) 1.3966 + || isHtmlElement(node.parentNode, ["ol", "ul"]) 1.3967 + || isAllowedChild(node, "li")); 1.3968 + }); 1.3969 + 1.3970 + // "If node list is empty, return "none"." 1.3971 + if (!nodeList.length) { 1.3972 + return "none"; 1.3973 + } 1.3974 + 1.3975 + // "If every member of node list is either an ol or the child of an ol or 1.3976 + // the child of an li child of an ol, and none is a ul or an ancestor of a 1.3977 + // ul, return "ol"." 1.3978 + if (nodeList.every(function(node) { 1.3979 + return isHtmlElement(node, "ol") 1.3980 + || isHtmlElement(node.parentNode, "ol") 1.3981 + || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); 1.3982 + }) 1.3983 + && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) { 1.3984 + return "ol"; 1.3985 + } 1.3986 + 1.3987 + // "If every member of node list is either a ul or the child of a ul or the 1.3988 + // child of an li child of a ul, and none is an ol or an ancestor of an ol, 1.3989 + // return "ul"." 1.3990 + if (nodeList.every(function(node) { 1.3991 + return isHtmlElement(node, "ul") 1.3992 + || isHtmlElement(node.parentNode, "ul") 1.3993 + || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); 1.3994 + }) 1.3995 + && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) { 1.3996 + return "ul"; 1.3997 + } 1.3998 + 1.3999 + var hasOl = nodeList.some(function(node) { 1.4000 + return isHtmlElement(node, "ol") 1.4001 + || isHtmlElement(node.parentNode, "ol") 1.4002 + || ("querySelector" in node && node.querySelector("ol")) 1.4003 + || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); 1.4004 + }); 1.4005 + var hasUl = nodeList.some(function(node) { 1.4006 + return isHtmlElement(node, "ul") 1.4007 + || isHtmlElement(node.parentNode, "ul") 1.4008 + || ("querySelector" in node && node.querySelector("ul")) 1.4009 + || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); 1.4010 + }); 1.4011 + // "If some member of node list is either an ol or the child or ancestor of 1.4012 + // an ol or the child of an li child of an ol, and some member of node list 1.4013 + // is either a ul or the child or ancestor of a ul or the child of an li 1.4014 + // child of a ul, return "mixed"." 1.4015 + if (hasOl && hasUl) { 1.4016 + return "mixed"; 1.4017 + } 1.4018 + 1.4019 + // "If some member of node list is either an ol or the child or ancestor of 1.4020 + // an ol or the child of an li child of an ol, return "mixed ol"." 1.4021 + if (hasOl) { 1.4022 + return "mixed ol"; 1.4023 + } 1.4024 + 1.4025 + // "If some member of node list is either a ul or the child or ancestor of 1.4026 + // a ul or the child of an li child of a ul, return "mixed ul"." 1.4027 + if (hasUl) { 1.4028 + return "mixed ul"; 1.4029 + } 1.4030 + 1.4031 + // "Return "none"." 1.4032 + return "none"; 1.4033 +} 1.4034 + 1.4035 +function getAlignmentValue(node) { 1.4036 + // "While node is neither null nor an Element, or it is an Element but its 1.4037 + // "display" property has resolved value "inline" or "none", set node to 1.4038 + // its parent." 1.4039 + while ((node && node.nodeType != Node.ELEMENT_NODE) 1.4040 + || (node.nodeType == Node.ELEMENT_NODE 1.4041 + && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) { 1.4042 + node = node.parentNode; 1.4043 + } 1.4044 + 1.4045 + // "If node is not an Element, return "left"." 1.4046 + if (!node || node.nodeType != Node.ELEMENT_NODE) { 1.4047 + return "left"; 1.4048 + } 1.4049 + 1.4050 + var resolvedValue = getComputedStyle(node).textAlign 1.4051 + // Hack around browser non-standardness 1.4052 + .replace(/^-(moz|webkit)-/, "") 1.4053 + .replace(/^auto$/, "start"); 1.4054 + 1.4055 + // "If node's "text-align" property has resolved value "start", return 1.4056 + // "left" if the directionality of node is "ltr", "right" if it is "rtl"." 1.4057 + if (resolvedValue == "start") { 1.4058 + return getDirectionality(node) == "ltr" ? "left" : "right"; 1.4059 + } 1.4060 + 1.4061 + // "If node's "text-align" property has resolved value "end", return 1.4062 + // "right" if the directionality of node is "ltr", "left" if it is "rtl"." 1.4063 + if (resolvedValue == "end") { 1.4064 + return getDirectionality(node) == "ltr" ? "right" : "left"; 1.4065 + } 1.4066 + 1.4067 + // "If node's "text-align" property has resolved value "center", "justify", 1.4068 + // "left", or "right", return that value." 1.4069 + if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) { 1.4070 + return resolvedValue; 1.4071 + } 1.4072 + 1.4073 + // "Return "left"." 1.4074 + return "left"; 1.4075 +} 1.4076 + 1.4077 +function getNextEquivalentPoint(node, offset) { 1.4078 + // "If node's length is zero, return null." 1.4079 + if (getNodeLength(node) == 0) { 1.4080 + return null; 1.4081 + } 1.4082 + 1.4083 + // "If offset is node's length, and node's parent is not null, and node is 1.4084 + // an inline node, return (node's parent, 1 + node's index)." 1.4085 + if (offset == getNodeLength(node) 1.4086 + && node.parentNode 1.4087 + && isInlineNode(node)) { 1.4088 + return [node.parentNode, 1 + getNodeIndex(node)]; 1.4089 + } 1.4090 + 1.4091 + // "If node has a child with index offset, and that child's length is not 1.4092 + // zero, and that child is an inline node, return (that child, 0)." 1.4093 + if (0 <= offset 1.4094 + && offset < node.childNodes.length 1.4095 + && getNodeLength(node.childNodes[offset]) != 0 1.4096 + && isInlineNode(node.childNodes[offset])) { 1.4097 + return [node.childNodes[offset], 0]; 1.4098 + } 1.4099 + 1.4100 + // "Return null." 1.4101 + return null; 1.4102 +} 1.4103 + 1.4104 +function getPreviousEquivalentPoint(node, offset) { 1.4105 + // "If node's length is zero, return null." 1.4106 + if (getNodeLength(node) == 0) { 1.4107 + return null; 1.4108 + } 1.4109 + 1.4110 + // "If offset is 0, and node's parent is not null, and node is an inline 1.4111 + // node, return (node's parent, node's index)." 1.4112 + if (offset == 0 1.4113 + && node.parentNode 1.4114 + && isInlineNode(node)) { 1.4115 + return [node.parentNode, getNodeIndex(node)]; 1.4116 + } 1.4117 + 1.4118 + // "If node has a child with index offset − 1, and that child's length is 1.4119 + // not zero, and that child is an inline node, return (that child, that 1.4120 + // child's length)." 1.4121 + if (0 <= offset - 1 1.4122 + && offset - 1 < node.childNodes.length 1.4123 + && getNodeLength(node.childNodes[offset - 1]) != 0 1.4124 + && isInlineNode(node.childNodes[offset - 1])) { 1.4125 + return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])]; 1.4126 + } 1.4127 + 1.4128 + // "Return null." 1.4129 + return null; 1.4130 +} 1.4131 + 1.4132 +function getFirstEquivalentPoint(node, offset) { 1.4133 + // "While (node, offset)'s previous equivalent point is not null, set 1.4134 + // (node, offset) to its previous equivalent point." 1.4135 + var prev; 1.4136 + while (prev = getPreviousEquivalentPoint(node, offset)) { 1.4137 + node = prev[0]; 1.4138 + offset = prev[1]; 1.4139 + } 1.4140 + 1.4141 + // "Return (node, offset)." 1.4142 + return [node, offset]; 1.4143 +} 1.4144 + 1.4145 +function getLastEquivalentPoint(node, offset) { 1.4146 + // "While (node, offset)'s next equivalent point is not null, set (node, 1.4147 + // offset) to its next equivalent point." 1.4148 + var next; 1.4149 + while (next = getNextEquivalentPoint(node, offset)) { 1.4150 + node = next[0]; 1.4151 + offset = next[1]; 1.4152 + } 1.4153 + 1.4154 + // "Return (node, offset)." 1.4155 + return [node, offset]; 1.4156 +} 1.4157 + 1.4158 +//@} 1.4159 +///// Block-extending a range ///// 1.4160 +//@{ 1.4161 + 1.4162 +// "A boundary point (node, offset) is a block start point if either node's 1.4163 +// parent is null and offset is zero; or node has a child with index offset − 1.4164 +// 1, and that child is either a visible block node or a visible br." 1.4165 +function isBlockStartPoint(node, offset) { 1.4166 + return (!node.parentNode && offset == 0) 1.4167 + || (0 <= offset - 1 1.4168 + && offset - 1 < node.childNodes.length 1.4169 + && isVisible(node.childNodes[offset - 1]) 1.4170 + && (isBlockNode(node.childNodes[offset - 1]) 1.4171 + || isHtmlElement(node.childNodes[offset - 1], "br"))); 1.4172 +} 1.4173 + 1.4174 +// "A boundary point (node, offset) is a block end point if either node's 1.4175 +// parent is null and offset is node's length; or node has a child with index 1.4176 +// offset, and that child is a visible block node." 1.4177 +function isBlockEndPoint(node, offset) { 1.4178 + return (!node.parentNode && offset == getNodeLength(node)) 1.4179 + || (offset < node.childNodes.length 1.4180 + && isVisible(node.childNodes[offset]) 1.4181 + && isBlockNode(node.childNodes[offset])); 1.4182 +} 1.4183 + 1.4184 +// "A boundary point is a block boundary point if it is either a block start 1.4185 +// point or a block end point." 1.4186 +function isBlockBoundaryPoint(node, offset) { 1.4187 + return isBlockStartPoint(node, offset) 1.4188 + || isBlockEndPoint(node, offset); 1.4189 +} 1.4190 + 1.4191 +function blockExtend(range) { 1.4192 + // "Let start node, start offset, end node, and end offset be the start 1.4193 + // and end nodes and offsets of the range." 1.4194 + var startNode = range.startContainer; 1.4195 + var startOffset = range.startOffset; 1.4196 + var endNode = range.endContainer; 1.4197 + var endOffset = range.endOffset; 1.4198 + 1.4199 + // "If some ancestor container of start node is an li, set start offset to 1.4200 + // the index of the last such li in tree order, and set start node to that 1.4201 + // li's parent." 1.4202 + var liAncestors = getAncestors(startNode).concat(startNode) 1.4203 + .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) 1.4204 + .slice(-1); 1.4205 + if (liAncestors.length) { 1.4206 + startOffset = getNodeIndex(liAncestors[0]); 1.4207 + startNode = liAncestors[0].parentNode; 1.4208 + } 1.4209 + 1.4210 + // "If (start node, start offset) is not a block start point, repeat the 1.4211 + // following steps:" 1.4212 + if (!isBlockStartPoint(startNode, startOffset)) do { 1.4213 + // "If start offset is zero, set it to start node's index, then set 1.4214 + // start node to its parent." 1.4215 + if (startOffset == 0) { 1.4216 + startOffset = getNodeIndex(startNode); 1.4217 + startNode = startNode.parentNode; 1.4218 + 1.4219 + // "Otherwise, subtract one from start offset." 1.4220 + } else { 1.4221 + startOffset--; 1.4222 + } 1.4223 + 1.4224 + // "If (start node, start offset) is a block boundary point, break from 1.4225 + // this loop." 1.4226 + } while (!isBlockBoundaryPoint(startNode, startOffset)); 1.4227 + 1.4228 + // "While start offset is zero and start node's parent is not null, set 1.4229 + // start offset to start node's index, then set start node to its parent." 1.4230 + while (startOffset == 0 1.4231 + && startNode.parentNode) { 1.4232 + startOffset = getNodeIndex(startNode); 1.4233 + startNode = startNode.parentNode; 1.4234 + } 1.4235 + 1.4236 + // "If some ancestor container of end node is an li, set end offset to one 1.4237 + // plus the index of the last such li in tree order, and set end node to 1.4238 + // that li's parent." 1.4239 + var liAncestors = getAncestors(endNode).concat(endNode) 1.4240 + .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) 1.4241 + .slice(-1); 1.4242 + if (liAncestors.length) { 1.4243 + endOffset = 1 + getNodeIndex(liAncestors[0]); 1.4244 + endNode = liAncestors[0].parentNode; 1.4245 + } 1.4246 + 1.4247 + // "If (end node, end offset) is not a block end point, repeat the 1.4248 + // following steps:" 1.4249 + if (!isBlockEndPoint(endNode, endOffset)) do { 1.4250 + // "If end offset is end node's length, set it to one plus end node's 1.4251 + // index, then set end node to its parent." 1.4252 + if (endOffset == getNodeLength(endNode)) { 1.4253 + endOffset = 1 + getNodeIndex(endNode); 1.4254 + endNode = endNode.parentNode; 1.4255 + 1.4256 + // "Otherwise, add one to end offset. 1.4257 + } else { 1.4258 + endOffset++; 1.4259 + } 1.4260 + 1.4261 + // "If (end node, end offset) is a block boundary point, break from 1.4262 + // this loop." 1.4263 + } while (!isBlockBoundaryPoint(endNode, endOffset)); 1.4264 + 1.4265 + // "While end offset is end node's length and end node's parent is not 1.4266 + // null, set end offset to one plus end node's index, then set end node to 1.4267 + // its parent." 1.4268 + while (endOffset == getNodeLength(endNode) 1.4269 + && endNode.parentNode) { 1.4270 + endOffset = 1 + getNodeIndex(endNode); 1.4271 + endNode = endNode.parentNode; 1.4272 + } 1.4273 + 1.4274 + // "Let new range be a new range whose start and end nodes and offsets 1.4275 + // are start node, start offset, end node, and end offset." 1.4276 + var newRange = startNode.ownerDocument.createRange(); 1.4277 + newRange.setStart(startNode, startOffset); 1.4278 + newRange.setEnd(endNode, endOffset); 1.4279 + 1.4280 + // "Return new range." 1.4281 + return newRange; 1.4282 +} 1.4283 + 1.4284 +function followsLineBreak(node) { 1.4285 + // "Let offset be zero." 1.4286 + var offset = 0; 1.4287 + 1.4288 + // "While (node, offset) is not a block boundary point:" 1.4289 + while (!isBlockBoundaryPoint(node, offset)) { 1.4290 + // "If node has a visible child with index offset minus one, return 1.4291 + // false." 1.4292 + if (0 <= offset - 1 1.4293 + && offset - 1 < node.childNodes.length 1.4294 + && isVisible(node.childNodes[offset - 1])) { 1.4295 + return false; 1.4296 + } 1.4297 + 1.4298 + // "If offset is zero or node has no children, set offset to node's 1.4299 + // index, then set node to its parent." 1.4300 + if (offset == 0 1.4301 + || !node.hasChildNodes()) { 1.4302 + offset = getNodeIndex(node); 1.4303 + node = node.parentNode; 1.4304 + 1.4305 + // "Otherwise, set node to its child with index offset minus one, then 1.4306 + // set offset to node's length." 1.4307 + } else { 1.4308 + node = node.childNodes[offset - 1]; 1.4309 + offset = getNodeLength(node); 1.4310 + } 1.4311 + } 1.4312 + 1.4313 + // "Return true." 1.4314 + return true; 1.4315 +} 1.4316 + 1.4317 +function precedesLineBreak(node) { 1.4318 + // "Let offset be node's length." 1.4319 + var offset = getNodeLength(node); 1.4320 + 1.4321 + // "While (node, offset) is not a block boundary point:" 1.4322 + while (!isBlockBoundaryPoint(node, offset)) { 1.4323 + // "If node has a visible child with index offset, return false." 1.4324 + if (offset < node.childNodes.length 1.4325 + && isVisible(node.childNodes[offset])) { 1.4326 + return false; 1.4327 + } 1.4328 + 1.4329 + // "If offset is node's length or node has no children, set offset to 1.4330 + // one plus node's index, then set node to its parent." 1.4331 + if (offset == getNodeLength(node) 1.4332 + || !node.hasChildNodes()) { 1.4333 + offset = 1 + getNodeIndex(node); 1.4334 + node = node.parentNode; 1.4335 + 1.4336 + // "Otherwise, set node to its child with index offset and set offset 1.4337 + // to zero." 1.4338 + } else { 1.4339 + node = node.childNodes[offset]; 1.4340 + offset = 0; 1.4341 + } 1.4342 + } 1.4343 + 1.4344 + // "Return true." 1.4345 + return true; 1.4346 +} 1.4347 + 1.4348 +//@} 1.4349 +///// Recording and restoring overrides ///// 1.4350 +//@{ 1.4351 + 1.4352 +function recordCurrentOverrides() { 1.4353 + // "Let overrides be a list of (string, string or boolean) ordered pairs, 1.4354 + // initially empty." 1.4355 + var overrides = []; 1.4356 + 1.4357 + // "If there is a value override for "createLink", add ("createLink", value 1.4358 + // override for "createLink") to overrides." 1.4359 + if (getValueOverride("createlink") !== undefined) { 1.4360 + overrides.push(["createlink", getValueOverride("createlink")]); 1.4361 + } 1.4362 + 1.4363 + // "For each command in the list "bold", "italic", "strikethrough", 1.4364 + // "subscript", "superscript", "underline", in order: if there is a state 1.4365 + // override for command, add (command, command's state override) to 1.4366 + // overrides." 1.4367 + ["bold", "italic", "strikethrough", "subscript", "superscript", 1.4368 + "underline"].forEach(function(command) { 1.4369 + if (getStateOverride(command) !== undefined) { 1.4370 + overrides.push([command, getStateOverride(command)]); 1.4371 + } 1.4372 + }); 1.4373 + 1.4374 + // "For each command in the list "fontName", "fontSize", "foreColor", 1.4375 + // "hiliteColor", in order: if there is a value override for command, add 1.4376 + // (command, command's value override) to overrides." 1.4377 + ["fontname", "fontsize", "forecolor", 1.4378 + "hilitecolor"].forEach(function(command) { 1.4379 + if (getValueOverride(command) !== undefined) { 1.4380 + overrides.push([command, getValueOverride(command)]); 1.4381 + } 1.4382 + }); 1.4383 + 1.4384 + // "Return overrides." 1.4385 + return overrides; 1.4386 +} 1.4387 + 1.4388 +function recordCurrentStatesAndValues() { 1.4389 + // "Let overrides be a list of (string, string or boolean) ordered pairs, 1.4390 + // initially empty." 1.4391 + var overrides = []; 1.4392 + 1.4393 + // "Let node be the first formattable node effectively contained in the 1.4394 + // active range, or null if there is none." 1.4395 + var node = getAllEffectivelyContainedNodes(getActiveRange()) 1.4396 + .filter(isFormattableNode)[0]; 1.4397 + 1.4398 + // "If node is null, return overrides." 1.4399 + if (!node) { 1.4400 + return overrides; 1.4401 + } 1.4402 + 1.4403 + // "Add ("createLink", node's effective command value for "createLink") to 1.4404 + // overrides." 1.4405 + overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]); 1.4406 + 1.4407 + // "For each command in the list "bold", "italic", "strikethrough", 1.4408 + // "subscript", "superscript", "underline", in order: if node's effective 1.4409 + // command value for command is one of its inline command activated values, 1.4410 + // add (command, true) to overrides, and otherwise add (command, false) to 1.4411 + // overrides." 1.4412 + ["bold", "italic", "strikethrough", "subscript", "superscript", 1.4413 + "underline"].forEach(function(command) { 1.4414 + if (commands[command].inlineCommandActivatedValues 1.4415 + .indexOf(getEffectiveCommandValue(node, command)) != -1) { 1.4416 + overrides.push([command, true]); 1.4417 + } else { 1.4418 + overrides.push([command, false]); 1.4419 + } 1.4420 + }); 1.4421 + 1.4422 + // "For each command in the list "fontName", "foreColor", "hiliteColor", in 1.4423 + // order: add (command, command's value) to overrides." 1.4424 + ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) { 1.4425 + overrides.push([command, commands[command].value()]); 1.4426 + }); 1.4427 + 1.4428 + // "Add ("fontSize", node's effective command value for "fontSize") to 1.4429 + // overrides." 1.4430 + overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); 1.4431 + 1.4432 + // "Return overrides." 1.4433 + return overrides; 1.4434 +} 1.4435 + 1.4436 +function restoreStatesAndValues(overrides) { 1.4437 + // "Let node be the first formattable node effectively contained in the 1.4438 + // active range, or null if there is none." 1.4439 + var node = getAllEffectivelyContainedNodes(getActiveRange()) 1.4440 + .filter(isFormattableNode)[0]; 1.4441 + 1.4442 + // "If node is not null, then for each (command, override) pair in 1.4443 + // overrides, in order:" 1.4444 + if (node) { 1.4445 + for (var i = 0; i < overrides.length; i++) { 1.4446 + var command = overrides[i][0]; 1.4447 + var override = overrides[i][1]; 1.4448 + 1.4449 + // "If override is a boolean, and queryCommandState(command) 1.4450 + // returns something different from override, take the action for 1.4451 + // command, with value equal to the empty string." 1.4452 + if (typeof override == "boolean" 1.4453 + && myQueryCommandState(command) != override) { 1.4454 + commands[command].action(""); 1.4455 + 1.4456 + // "Otherwise, if override is a string, and command is neither 1.4457 + // "createLink" nor "fontSize", and queryCommandValue(command) 1.4458 + // returns something not equivalent to override, take the action 1.4459 + // for command, with value equal to override." 1.4460 + } else if (typeof override == "string" 1.4461 + && command != "createlink" 1.4462 + && command != "fontsize" 1.4463 + && !areEquivalentValues(command, myQueryCommandValue(command), override)) { 1.4464 + commands[command].action(override); 1.4465 + 1.4466 + // "Otherwise, if override is a string; and command is 1.4467 + // "createLink"; and either there is a value override for 1.4468 + // "createLink" that is not equal to override, or there is no value 1.4469 + // override for "createLink" and node's effective command value for 1.4470 + // "createLink" is not equal to override: take the action for 1.4471 + // "createLink", with value equal to override." 1.4472 + } else if (typeof override == "string" 1.4473 + && command == "createlink" 1.4474 + && ( 1.4475 + ( 1.4476 + getValueOverride("createlink") !== undefined 1.4477 + && getValueOverride("createlink") !== override 1.4478 + ) || ( 1.4479 + getValueOverride("createlink") === undefined 1.4480 + && getEffectiveCommandValue(node, "createlink") !== override 1.4481 + ) 1.4482 + )) { 1.4483 + commands.createlink.action(override); 1.4484 + 1.4485 + // "Otherwise, if override is a string; and command is "fontSize"; 1.4486 + // and either there is a value override for "fontSize" that is not 1.4487 + // equal to override, or there is no value override for "fontSize" 1.4488 + // and node's effective command value for "fontSize" is not loosely 1.4489 + // equivalent to override:" 1.4490 + } else if (typeof override == "string" 1.4491 + && command == "fontsize" 1.4492 + && ( 1.4493 + ( 1.4494 + getValueOverride("fontsize") !== undefined 1.4495 + && getValueOverride("fontsize") !== override 1.4496 + ) || ( 1.4497 + getValueOverride("fontsize") === undefined 1.4498 + && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override) 1.4499 + ) 1.4500 + )) { 1.4501 + // "Convert override to an integer number of pixels, and set 1.4502 + // override to the legacy font size for the result." 1.4503 + override = getLegacyFontSize(override); 1.4504 + 1.4505 + // "Take the action for "fontSize", with value equal to 1.4506 + // override." 1.4507 + commands.fontsize.action(override); 1.4508 + 1.4509 + // "Otherwise, continue this loop from the beginning." 1.4510 + } else { 1.4511 + continue; 1.4512 + } 1.4513 + 1.4514 + // "Set node to the first formattable node effectively contained in 1.4515 + // the active range, if there is one." 1.4516 + node = getAllEffectivelyContainedNodes(getActiveRange()) 1.4517 + .filter(isFormattableNode)[0] 1.4518 + || node; 1.4519 + } 1.4520 + 1.4521 + // "Otherwise, for each (command, override) pair in overrides, in order:" 1.4522 + } else { 1.4523 + for (var i = 0; i < overrides.length; i++) { 1.4524 + var command = overrides[i][0]; 1.4525 + var override = overrides[i][1]; 1.4526 + 1.4527 + // "If override is a boolean, set the state override for command to 1.4528 + // override." 1.4529 + if (typeof override == "boolean") { 1.4530 + setStateOverride(command, override); 1.4531 + } 1.4532 + 1.4533 + // "If override is a string, set the value override for command to 1.4534 + // override." 1.4535 + if (typeof override == "string") { 1.4536 + setValueOverride(command, override); 1.4537 + } 1.4538 + } 1.4539 + } 1.4540 +} 1.4541 + 1.4542 +//@} 1.4543 +///// Deleting the selection ///// 1.4544 +//@{ 1.4545 + 1.4546 +// The flags argument is a dictionary that can have blockMerging, 1.4547 +// stripWrappers, and/or direction as keys. 1.4548 +function deleteSelection(flags) { 1.4549 + if (flags === undefined) { 1.4550 + flags = {}; 1.4551 + } 1.4552 + 1.4553 + var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true; 1.4554 + var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true; 1.4555 + var direction = "direction" in flags ? flags.direction : "forward"; 1.4556 + 1.4557 + // "If the active range is null, abort these steps and do nothing." 1.4558 + if (!getActiveRange()) { 1.4559 + return; 1.4560 + } 1.4561 + 1.4562 + // "Canonicalize whitespace at the active range's start." 1.4563 + canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); 1.4564 + 1.4565 + // "Canonicalize whitespace at the active range's end." 1.4566 + canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset); 1.4567 + 1.4568 + // "Let (start node, start offset) be the last equivalent point for the 1.4569 + // active range's start." 1.4570 + var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset); 1.4571 + var startNode = start[0]; 1.4572 + var startOffset = start[1]; 1.4573 + 1.4574 + // "Let (end node, end offset) be the first equivalent point for the active 1.4575 + // range's end." 1.4576 + var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset); 1.4577 + var endNode = end[0]; 1.4578 + var endOffset = end[1]; 1.4579 + 1.4580 + // "If (end node, end offset) is not after (start node, start offset):" 1.4581 + if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { 1.4582 + // "If direction is "forward", call collapseToStart() on the context 1.4583 + // object's Selection." 1.4584 + // 1.4585 + // Here and in a few other places, we check rangeCount to work around a 1.4586 + // WebKit bug: it will sometimes incorrectly remove ranges from the 1.4587 + // selection if nodes are removed, so collapseToStart() will throw. 1.4588 + // This will break everything if we're using an actual selection, but 1.4589 + // if getActiveRange() is really just returning globalRange and that's 1.4590 + // all we care about, it will work fine. I only add the extra check 1.4591 + // for errors I actually hit in testing. 1.4592 + if (direction == "forward") { 1.4593 + if (getSelection().rangeCount) { 1.4594 + getSelection().collapseToStart(); 1.4595 + } 1.4596 + getActiveRange().collapse(true); 1.4597 + 1.4598 + // "Otherwise, call collapseToEnd() on the context object's Selection." 1.4599 + } else { 1.4600 + getSelection().collapseToEnd(); 1.4601 + getActiveRange().collapse(false); 1.4602 + } 1.4603 + 1.4604 + // "Abort these steps." 1.4605 + return; 1.4606 + } 1.4607 + 1.4608 + // "If start node is a Text node and start offset is 0, set start offset to 1.4609 + // the index of start node, then set start node to its parent." 1.4610 + if (startNode.nodeType == Node.TEXT_NODE 1.4611 + && startOffset == 0) { 1.4612 + startOffset = getNodeIndex(startNode); 1.4613 + startNode = startNode.parentNode; 1.4614 + } 1.4615 + 1.4616 + // "If end node is a Text node and end offset is its length, set end offset 1.4617 + // to one plus the index of end node, then set end node to its parent." 1.4618 + if (endNode.nodeType == Node.TEXT_NODE 1.4619 + && endOffset == getNodeLength(endNode)) { 1.4620 + endOffset = 1 + getNodeIndex(endNode); 1.4621 + endNode = endNode.parentNode; 1.4622 + } 1.4623 + 1.4624 + // "Call collapse(start node, start offset) on the context object's 1.4625 + // Selection." 1.4626 + getSelection().collapse(startNode, startOffset); 1.4627 + getActiveRange().setStart(startNode, startOffset); 1.4628 + 1.4629 + // "Call extend(end node, end offset) on the context object's Selection." 1.4630 + getSelection().extend(endNode, endOffset); 1.4631 + getActiveRange().setEnd(endNode, endOffset); 1.4632 + 1.4633 + // "Let start block be the active range's start node." 1.4634 + var startBlock = getActiveRange().startContainer; 1.4635 + 1.4636 + // "While start block's parent is in the same editing host and start block 1.4637 + // is an inline node, set start block to its parent." 1.4638 + while (inSameEditingHost(startBlock, startBlock.parentNode) 1.4639 + && isInlineNode(startBlock)) { 1.4640 + startBlock = startBlock.parentNode; 1.4641 + } 1.4642 + 1.4643 + // "If start block is neither a block node nor an editing host, or "span" 1.4644 + // is not an allowed child of start block, or start block is a td or th, 1.4645 + // set start block to null." 1.4646 + if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) 1.4647 + || !isAllowedChild("span", startBlock) 1.4648 + || isHtmlElement(startBlock, ["td", "th"])) { 1.4649 + startBlock = null; 1.4650 + } 1.4651 + 1.4652 + // "Let end block be the active range's end node." 1.4653 + var endBlock = getActiveRange().endContainer; 1.4654 + 1.4655 + // "While end block's parent is in the same editing host and end block is 1.4656 + // an inline node, set end block to its parent." 1.4657 + while (inSameEditingHost(endBlock, endBlock.parentNode) 1.4658 + && isInlineNode(endBlock)) { 1.4659 + endBlock = endBlock.parentNode; 1.4660 + } 1.4661 + 1.4662 + // "If end block is neither a block node nor an editing host, or "span" is 1.4663 + // not an allowed child of end block, or end block is a td or th, set end 1.4664 + // block to null." 1.4665 + if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) 1.4666 + || !isAllowedChild("span", endBlock) 1.4667 + || isHtmlElement(endBlock, ["td", "th"])) { 1.4668 + endBlock = null; 1.4669 + } 1.4670 + 1.4671 + // "Record current states and values, and let overrides be the result." 1.4672 + var overrides = recordCurrentStatesAndValues(); 1.4673 + 1.4674 + // "If start node and end node are the same, and start node is an editable 1.4675 + // Text node:" 1.4676 + if (startNode == endNode 1.4677 + && isEditable(startNode) 1.4678 + && startNode.nodeType == Node.TEXT_NODE) { 1.4679 + // "Call deleteData(start offset, end offset − start offset) on start 1.4680 + // node." 1.4681 + startNode.deleteData(startOffset, endOffset - startOffset); 1.4682 + 1.4683 + // "Canonicalize whitespace at (start node, start offset), with fix 1.4684 + // collapsed space false." 1.4685 + canonicalizeWhitespace(startNode, startOffset, false); 1.4686 + 1.4687 + // "If direction is "forward", call collapseToStart() on the context 1.4688 + // object's Selection." 1.4689 + if (direction == "forward") { 1.4690 + if (getSelection().rangeCount) { 1.4691 + getSelection().collapseToStart(); 1.4692 + } 1.4693 + getActiveRange().collapse(true); 1.4694 + 1.4695 + // "Otherwise, call collapseToEnd() on the context object's Selection." 1.4696 + } else { 1.4697 + getSelection().collapseToEnd(); 1.4698 + getActiveRange().collapse(false); 1.4699 + } 1.4700 + 1.4701 + // "Restore states and values from overrides." 1.4702 + restoreStatesAndValues(overrides); 1.4703 + 1.4704 + // "Abort these steps." 1.4705 + return; 1.4706 + } 1.4707 + 1.4708 + // "If start node is an editable Text node, call deleteData() on it, with 1.4709 + // start offset as the first argument and (length of start node − start 1.4710 + // offset) as the second argument." 1.4711 + if (isEditable(startNode) 1.4712 + && startNode.nodeType == Node.TEXT_NODE) { 1.4713 + startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset); 1.4714 + } 1.4715 + 1.4716 + // "Let node list be a list of nodes, initially empty." 1.4717 + // 1.4718 + // "For each node contained in the active range, append node to node list 1.4719 + // if the last member of node list (if any) is not an ancestor of node; 1.4720 + // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td." 1.4721 + var nodeList = getContainedNodes(getActiveRange(), 1.4722 + function(node) { 1.4723 + return isEditable(node) 1.4724 + && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]); 1.4725 + } 1.4726 + ); 1.4727 + 1.4728 + // "For each node in node list:" 1.4729 + for (var i = 0; i < nodeList.length; i++) { 1.4730 + var node = nodeList[i]; 1.4731 + 1.4732 + // "Let parent be the parent of node." 1.4733 + var parent_ = node.parentNode; 1.4734 + 1.4735 + // "Remove node from parent." 1.4736 + parent_.removeChild(node); 1.4737 + 1.4738 + // "If the block node of parent has no visible children, and parent is 1.4739 + // editable or an editing host, call createElement("br") on the context 1.4740 + // object and append the result as the last child of parent." 1.4741 + if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible) 1.4742 + && (isEditable(parent_) || isEditingHost(parent_))) { 1.4743 + parent_.appendChild(document.createElement("br")); 1.4744 + } 1.4745 + 1.4746 + // "If strip wrappers is true or parent is not an ancestor container of 1.4747 + // start node, while parent is an editable inline node with length 0, 1.4748 + // let grandparent be the parent of parent, then remove parent from 1.4749 + // grandparent, then set parent to grandparent." 1.4750 + if (stripWrappers 1.4751 + || (!isAncestor(parent_, startNode) && parent_ != startNode)) { 1.4752 + while (isEditable(parent_) 1.4753 + && isInlineNode(parent_) 1.4754 + && getNodeLength(parent_) == 0) { 1.4755 + var grandparent = parent_.parentNode; 1.4756 + grandparent.removeChild(parent_); 1.4757 + parent_ = grandparent; 1.4758 + } 1.4759 + } 1.4760 + } 1.4761 + 1.4762 + // "If end node is an editable Text node, call deleteData(0, end offset) on 1.4763 + // it." 1.4764 + if (isEditable(endNode) 1.4765 + && endNode.nodeType == Node.TEXT_NODE) { 1.4766 + endNode.deleteData(0, endOffset); 1.4767 + } 1.4768 + 1.4769 + // "Canonicalize whitespace at the active range's start, with fix collapsed 1.4770 + // space false." 1.4771 + canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); 1.4772 + 1.4773 + // "Canonicalize whitespace at the active range's end, with fix collapsed 1.4774 + // space false." 1.4775 + canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); 1.4776 + 1.4777 + // "If block merging is false, or start block or end block is null, or 1.4778 + // start block is not in the same editing host as end block, or start block 1.4779 + // and end block are the same:" 1.4780 + if (!blockMerging 1.4781 + || !startBlock 1.4782 + || !endBlock 1.4783 + || !inSameEditingHost(startBlock, endBlock) 1.4784 + || startBlock == endBlock) { 1.4785 + // "If direction is "forward", call collapseToStart() on the context 1.4786 + // object's Selection." 1.4787 + if (direction == "forward") { 1.4788 + if (getSelection().rangeCount) { 1.4789 + getSelection().collapseToStart(); 1.4790 + } 1.4791 + getActiveRange().collapse(true); 1.4792 + 1.4793 + // "Otherwise, call collapseToEnd() on the context object's Selection." 1.4794 + } else { 1.4795 + if (getSelection().rangeCount) { 1.4796 + getSelection().collapseToEnd(); 1.4797 + } 1.4798 + getActiveRange().collapse(false); 1.4799 + } 1.4800 + 1.4801 + // "Restore states and values from overrides." 1.4802 + restoreStatesAndValues(overrides); 1.4803 + 1.4804 + // "Abort these steps." 1.4805 + return; 1.4806 + } 1.4807 + 1.4808 + // "If start block has one child, which is a collapsed block prop, remove 1.4809 + // its child from it." 1.4810 + if (startBlock.children.length == 1 1.4811 + && isCollapsedBlockProp(startBlock.firstChild)) { 1.4812 + startBlock.removeChild(startBlock.firstChild); 1.4813 + } 1.4814 + 1.4815 + // "If start block is an ancestor of end block:" 1.4816 + if (isAncestor(startBlock, endBlock)) { 1.4817 + // "Let reference node be end block." 1.4818 + var referenceNode = endBlock; 1.4819 + 1.4820 + // "While reference node is not a child of start block, set reference 1.4821 + // node to its parent." 1.4822 + while (referenceNode.parentNode != startBlock) { 1.4823 + referenceNode = referenceNode.parentNode; 1.4824 + } 1.4825 + 1.4826 + // "Call collapse() on the context object's Selection, with first 1.4827 + // argument start block and second argument the index of reference 1.4828 + // node." 1.4829 + getSelection().collapse(startBlock, getNodeIndex(referenceNode)); 1.4830 + getActiveRange().setStart(startBlock, getNodeIndex(referenceNode)); 1.4831 + getActiveRange().collapse(true); 1.4832 + 1.4833 + // "If end block has no children:" 1.4834 + if (!endBlock.hasChildNodes()) { 1.4835 + // "While end block is editable and is the only child of its parent 1.4836 + // and is not a child of start block, let parent equal end block, 1.4837 + // then remove end block from parent, then set end block to 1.4838 + // parent." 1.4839 + while (isEditable(endBlock) 1.4840 + && endBlock.parentNode.childNodes.length == 1 1.4841 + && endBlock.parentNode != startBlock) { 1.4842 + var parent_ = endBlock; 1.4843 + parent_.removeChild(endBlock); 1.4844 + endBlock = parent_; 1.4845 + } 1.4846 + 1.4847 + // "If end block is editable and is not an inline node, and its 1.4848 + // previousSibling and nextSibling are both inline nodes, call 1.4849 + // createElement("br") on the context object and insert it into end 1.4850 + // block's parent immediately after end block." 1.4851 + if (isEditable(endBlock) 1.4852 + && !isInlineNode(endBlock) 1.4853 + && isInlineNode(endBlock.previousSibling) 1.4854 + && isInlineNode(endBlock.nextSibling)) { 1.4855 + endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling); 1.4856 + } 1.4857 + 1.4858 + // "If end block is editable, remove it from its parent." 1.4859 + if (isEditable(endBlock)) { 1.4860 + endBlock.parentNode.removeChild(endBlock); 1.4861 + } 1.4862 + 1.4863 + // "Restore states and values from overrides." 1.4864 + restoreStatesAndValues(overrides); 1.4865 + 1.4866 + // "Abort these steps." 1.4867 + return; 1.4868 + } 1.4869 + 1.4870 + // "If end block's firstChild is not an inline node, restore states and 1.4871 + // values from overrides, then abort these steps." 1.4872 + if (!isInlineNode(endBlock.firstChild)) { 1.4873 + restoreStatesAndValues(overrides); 1.4874 + return; 1.4875 + } 1.4876 + 1.4877 + // "Let children be a list of nodes, initially empty." 1.4878 + var children = []; 1.4879 + 1.4880 + // "Append the first child of end block to children." 1.4881 + children.push(endBlock.firstChild); 1.4882 + 1.4883 + // "While children's last member is not a br, and children's last 1.4884 + // member's nextSibling is an inline node, append children's last 1.4885 + // member's nextSibling to children." 1.4886 + while (!isHtmlElement(children[children.length - 1], "br") 1.4887 + && isInlineNode(children[children.length - 1].nextSibling)) { 1.4888 + children.push(children[children.length - 1].nextSibling); 1.4889 + } 1.4890 + 1.4891 + // "Record the values of children, and let values be the result." 1.4892 + var values = recordValues(children); 1.4893 + 1.4894 + // "While children's first member's parent is not start block, split 1.4895 + // the parent of children." 1.4896 + while (children[0].parentNode != startBlock) { 1.4897 + splitParent(children); 1.4898 + } 1.4899 + 1.4900 + // "If children's first member's previousSibling is an editable br, 1.4901 + // remove that br from its parent." 1.4902 + if (isEditable(children[0].previousSibling) 1.4903 + && isHtmlElement(children[0].previousSibling, "br")) { 1.4904 + children[0].parentNode.removeChild(children[0].previousSibling); 1.4905 + } 1.4906 + 1.4907 + // "Otherwise, if start block is a descendant of end block:" 1.4908 + } else if (isDescendant(startBlock, endBlock)) { 1.4909 + // "Call collapse() on the context object's Selection, with first 1.4910 + // argument start block and second argument start block's length." 1.4911 + getSelection().collapse(startBlock, getNodeLength(startBlock)); 1.4912 + getActiveRange().setStart(startBlock, getNodeLength(startBlock)); 1.4913 + getActiveRange().collapse(true); 1.4914 + 1.4915 + // "Let reference node be start block." 1.4916 + var referenceNode = startBlock; 1.4917 + 1.4918 + // "While reference node is not a child of end block, set reference 1.4919 + // node to its parent." 1.4920 + while (referenceNode.parentNode != endBlock) { 1.4921 + referenceNode = referenceNode.parentNode; 1.4922 + } 1.4923 + 1.4924 + // "If reference node's nextSibling is an inline node and start block's 1.4925 + // lastChild is a br, remove start block's lastChild from it." 1.4926 + if (isInlineNode(referenceNode.nextSibling) 1.4927 + && isHtmlElement(startBlock.lastChild, "br")) { 1.4928 + startBlock.removeChild(startBlock.lastChild); 1.4929 + } 1.4930 + 1.4931 + // "Let nodes to move be a list of nodes, initially empty." 1.4932 + var nodesToMove = []; 1.4933 + 1.4934 + // "If reference node's nextSibling is neither null nor a block node, 1.4935 + // append it to nodes to move." 1.4936 + if (referenceNode.nextSibling 1.4937 + && !isBlockNode(referenceNode.nextSibling)) { 1.4938 + nodesToMove.push(referenceNode.nextSibling); 1.4939 + } 1.4940 + 1.4941 + // "While nodes to move is nonempty and its last member isn't a br and 1.4942 + // its last member's nextSibling is neither null nor a block node, 1.4943 + // append its last member's nextSibling to nodes to move." 1.4944 + if (nodesToMove.length 1.4945 + && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br") 1.4946 + && nodesToMove[nodesToMove.length - 1].nextSibling 1.4947 + && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { 1.4948 + nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); 1.4949 + } 1.4950 + 1.4951 + // "Record the values of nodes to move, and let values be the result." 1.4952 + var values = recordValues(nodesToMove); 1.4953 + 1.4954 + // "For each node in nodes to move, append node as the last child of 1.4955 + // start block, preserving ranges." 1.4956 + nodesToMove.forEach(function(node) { 1.4957 + movePreservingRanges(node, startBlock, -1); 1.4958 + }); 1.4959 + 1.4960 + // "Otherwise:" 1.4961 + } else { 1.4962 + // "Call collapse() on the context object's Selection, with first 1.4963 + // argument start block and second argument start block's length." 1.4964 + getSelection().collapse(startBlock, getNodeLength(startBlock)); 1.4965 + getActiveRange().setStart(startBlock, getNodeLength(startBlock)); 1.4966 + getActiveRange().collapse(true); 1.4967 + 1.4968 + // "If end block's firstChild is an inline node and start block's 1.4969 + // lastChild is a br, remove start block's lastChild from it." 1.4970 + if (isInlineNode(endBlock.firstChild) 1.4971 + && isHtmlElement(startBlock.lastChild, "br")) { 1.4972 + startBlock.removeChild(startBlock.lastChild); 1.4973 + } 1.4974 + 1.4975 + // "Record the values of end block's children, and let values be the 1.4976 + // result." 1.4977 + var values = recordValues([].slice.call(endBlock.childNodes)); 1.4978 + 1.4979 + // "While end block has children, append the first child of end block 1.4980 + // to start block, preserving ranges." 1.4981 + while (endBlock.hasChildNodes()) { 1.4982 + movePreservingRanges(endBlock.firstChild, startBlock, -1); 1.4983 + } 1.4984 + 1.4985 + // "While end block has no children, let parent be the parent of end 1.4986 + // block, then remove end block from parent, then set end block to 1.4987 + // parent." 1.4988 + while (!endBlock.hasChildNodes()) { 1.4989 + var parent_ = endBlock.parentNode; 1.4990 + parent_.removeChild(endBlock); 1.4991 + endBlock = parent_; 1.4992 + } 1.4993 + } 1.4994 + 1.4995 + // "Let ancestor be start block." 1.4996 + var ancestor = startBlock; 1.4997 + 1.4998 + // "While ancestor has an inclusive ancestor ol in the same editing host 1.4999 + // whose nextSibling is also an ol in the same editing host, or an 1.5000 + // inclusive ancestor ul in the same editing host whose nextSibling is also 1.5001 + // a ul in the same editing host:" 1.5002 + while (getInclusiveAncestors(ancestor).some(function(node) { 1.5003 + return inSameEditingHost(ancestor, node) 1.5004 + && ( 1.5005 + (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol")) 1.5006 + || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul")) 1.5007 + ) && inSameEditingHost(ancestor, node.nextSibling); 1.5008 + })) { 1.5009 + // "While ancestor and its nextSibling are not both ols in the same 1.5010 + // editing host, and are also not both uls in the same editing host, 1.5011 + // set ancestor to its parent." 1.5012 + while (!( 1.5013 + isHtmlElement(ancestor, "ol") 1.5014 + && isHtmlElement(ancestor.nextSibling, "ol") 1.5015 + && inSameEditingHost(ancestor, ancestor.nextSibling) 1.5016 + ) && !( 1.5017 + isHtmlElement(ancestor, "ul") 1.5018 + && isHtmlElement(ancestor.nextSibling, "ul") 1.5019 + && inSameEditingHost(ancestor, ancestor.nextSibling) 1.5020 + )) { 1.5021 + ancestor = ancestor.parentNode; 1.5022 + } 1.5023 + 1.5024 + // "While ancestor's nextSibling has children, append ancestor's 1.5025 + // nextSibling's firstChild as the last child of ancestor, preserving 1.5026 + // ranges." 1.5027 + while (ancestor.nextSibling.hasChildNodes()) { 1.5028 + movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1); 1.5029 + } 1.5030 + 1.5031 + // "Remove ancestor's nextSibling from its parent." 1.5032 + ancestor.parentNode.removeChild(ancestor.nextSibling); 1.5033 + } 1.5034 + 1.5035 + // "Restore the values from values." 1.5036 + restoreValues(values); 1.5037 + 1.5038 + // "If start block has no children, call createElement("br") on the context 1.5039 + // object and append the result as the last child of start block." 1.5040 + if (!startBlock.hasChildNodes()) { 1.5041 + startBlock.appendChild(document.createElement("br")); 1.5042 + } 1.5043 + 1.5044 + // "Remove extraneous line breaks at the end of start block." 1.5045 + removeExtraneousLineBreaksAtTheEndOf(startBlock); 1.5046 + 1.5047 + // "Restore states and values from overrides." 1.5048 + restoreStatesAndValues(overrides); 1.5049 +} 1.5050 + 1.5051 + 1.5052 +//@} 1.5053 +///// Splitting a node list's parent ///// 1.5054 +//@{ 1.5055 + 1.5056 +function splitParent(nodeList) { 1.5057 + // "Let original parent be the parent of the first member of node list." 1.5058 + var originalParent = nodeList[0].parentNode; 1.5059 + 1.5060 + // "If original parent is not editable or its parent is null, do nothing 1.5061 + // and abort these steps." 1.5062 + if (!isEditable(originalParent) 1.5063 + || !originalParent.parentNode) { 1.5064 + return; 1.5065 + } 1.5066 + 1.5067 + // "If the first child of original parent is in node list, remove 1.5068 + // extraneous line breaks before original parent." 1.5069 + if (nodeList.indexOf(originalParent.firstChild) != -1) { 1.5070 + removeExtraneousLineBreaksBefore(originalParent); 1.5071 + } 1.5072 + 1.5073 + // "If the first child of original parent is in node list, and original 1.5074 + // parent follows a line break, set follows line break to true. Otherwise, 1.5075 + // set follows line break to false." 1.5076 + var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1 1.5077 + && followsLineBreak(originalParent); 1.5078 + 1.5079 + // "If the last child of original parent is in node list, and original 1.5080 + // parent precedes a line break, set precedes line break to true. 1.5081 + // Otherwise, set precedes line break to false." 1.5082 + var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1 1.5083 + && precedesLineBreak(originalParent); 1.5084 + 1.5085 + // "If the first child of original parent is not in node list, but its last 1.5086 + // child is:" 1.5087 + if (nodeList.indexOf(originalParent.firstChild) == -1 1.5088 + && nodeList.indexOf(originalParent.lastChild) != -1) { 1.5089 + // "For each node in node list, in reverse order, insert node into the 1.5090 + // parent of original parent immediately after original parent, 1.5091 + // preserving ranges." 1.5092 + for (var i = nodeList.length - 1; i >= 0; i--) { 1.5093 + movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent)); 1.5094 + } 1.5095 + 1.5096 + // "If precedes line break is true, and the last member of node list 1.5097 + // does not precede a line break, call createElement("br") on the 1.5098 + // context object and insert the result immediately after the last 1.5099 + // member of node list." 1.5100 + if (precedesLineBreak_ 1.5101 + && !precedesLineBreak(nodeList[nodeList.length - 1])) { 1.5102 + nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 1.5103 + } 1.5104 + 1.5105 + // "Remove extraneous line breaks at the end of original parent." 1.5106 + removeExtraneousLineBreaksAtTheEndOf(originalParent); 1.5107 + 1.5108 + // "Abort these steps." 1.5109 + return; 1.5110 + } 1.5111 + 1.5112 + // "If the first child of original parent is not in node list:" 1.5113 + if (nodeList.indexOf(originalParent.firstChild) == -1) { 1.5114 + // "Let cloned parent be the result of calling cloneNode(false) on 1.5115 + // original parent." 1.5116 + var clonedParent = originalParent.cloneNode(false); 1.5117 + 1.5118 + // "If original parent has an id attribute, unset it." 1.5119 + originalParent.removeAttribute("id"); 1.5120 + 1.5121 + // "Insert cloned parent into the parent of original parent immediately 1.5122 + // before original parent." 1.5123 + originalParent.parentNode.insertBefore(clonedParent, originalParent); 1.5124 + 1.5125 + // "While the previousSibling of the first member of node list is not 1.5126 + // null, append the first child of original parent as the last child of 1.5127 + // cloned parent, preserving ranges." 1.5128 + while (nodeList[0].previousSibling) { 1.5129 + movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length); 1.5130 + } 1.5131 + } 1.5132 + 1.5133 + // "For each node in node list, insert node into the parent of original 1.5134 + // parent immediately before original parent, preserving ranges." 1.5135 + for (var i = 0; i < nodeList.length; i++) { 1.5136 + movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent)); 1.5137 + } 1.5138 + 1.5139 + // "If follows line break is true, and the first member of node list does 1.5140 + // not follow a line break, call createElement("br") on the context object 1.5141 + // and insert the result immediately before the first member of node list." 1.5142 + if (followsLineBreak_ 1.5143 + && !followsLineBreak(nodeList[0])) { 1.5144 + nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]); 1.5145 + } 1.5146 + 1.5147 + // "If the last member of node list is an inline node other than a br, and 1.5148 + // the first child of original parent is a br, and original parent is not 1.5149 + // an inline node, remove the first child of original parent from original 1.5150 + // parent." 1.5151 + if (isInlineNode(nodeList[nodeList.length - 1]) 1.5152 + && !isHtmlElement(nodeList[nodeList.length - 1], "br") 1.5153 + && isHtmlElement(originalParent.firstChild, "br") 1.5154 + && !isInlineNode(originalParent)) { 1.5155 + originalParent.removeChild(originalParent.firstChild); 1.5156 + } 1.5157 + 1.5158 + // "If original parent has no children:" 1.5159 + if (!originalParent.hasChildNodes()) { 1.5160 + // "Remove original parent from its parent." 1.5161 + originalParent.parentNode.removeChild(originalParent); 1.5162 + 1.5163 + // "If precedes line break is true, and the last member of node list 1.5164 + // does not precede a line break, call createElement("br") on the 1.5165 + // context object and insert the result immediately after the last 1.5166 + // member of node list." 1.5167 + if (precedesLineBreak_ 1.5168 + && !precedesLineBreak(nodeList[nodeList.length - 1])) { 1.5169 + nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 1.5170 + } 1.5171 + 1.5172 + // "Otherwise, remove extraneous line breaks before original parent." 1.5173 + } else { 1.5174 + removeExtraneousLineBreaksBefore(originalParent); 1.5175 + } 1.5176 + 1.5177 + // "If node list's last member's nextSibling is null, but its parent is not 1.5178 + // null, remove extraneous line breaks at the end of node list's last 1.5179 + // member's parent." 1.5180 + if (!nodeList[nodeList.length - 1].nextSibling 1.5181 + && nodeList[nodeList.length - 1].parentNode) { 1.5182 + removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode); 1.5183 + } 1.5184 +} 1.5185 + 1.5186 +// "To remove a node node while preserving its descendants, split the parent of 1.5187 +// node's children if it has any. If it has no children, instead remove it from 1.5188 +// its parent." 1.5189 +function removePreservingDescendants(node) { 1.5190 + if (node.hasChildNodes()) { 1.5191 + splitParent([].slice.call(node.childNodes)); 1.5192 + } else { 1.5193 + node.parentNode.removeChild(node); 1.5194 + } 1.5195 +} 1.5196 + 1.5197 + 1.5198 +//@} 1.5199 +///// Canonical space sequences ///// 1.5200 +//@{ 1.5201 + 1.5202 +function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { 1.5203 + // "If n is zero, return the empty string." 1.5204 + if (n == 0) { 1.5205 + return ""; 1.5206 + } 1.5207 + 1.5208 + // "If n is one and both non-breaking start and non-breaking end are false, 1.5209 + // return a single space (U+0020)." 1.5210 + if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { 1.5211 + return " "; 1.5212 + } 1.5213 + 1.5214 + // "If n is one, return a single non-breaking space (U+00A0)." 1.5215 + if (n == 1) { 1.5216 + return "\xa0"; 1.5217 + } 1.5218 + 1.5219 + // "Let buffer be the empty string." 1.5220 + var buffer = ""; 1.5221 + 1.5222 + // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. 1.5223 + // Otherwise, let it be U+0020 U+00A0." 1.5224 + var repeatedPair; 1.5225 + if (nonBreakingStart) { 1.5226 + repeatedPair = "\xa0 "; 1.5227 + } else { 1.5228 + repeatedPair = " \xa0"; 1.5229 + } 1.5230 + 1.5231 + // "While n is greater than three, append repeated pair to buffer and 1.5232 + // subtract two from n." 1.5233 + while (n > 3) { 1.5234 + buffer += repeatedPair; 1.5235 + n -= 2; 1.5236 + } 1.5237 + 1.5238 + // "If n is three, append a three-element string to buffer depending on 1.5239 + // non-breaking start and non-breaking end:" 1.5240 + if (n == 3) { 1.5241 + buffer += 1.5242 + !nonBreakingStart && !nonBreakingEnd ? " \xa0 " 1.5243 + : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " 1.5244 + : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" 1.5245 + : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" 1.5246 + : "impossible"; 1.5247 + 1.5248 + // "Otherwise, append a two-element string to buffer depending on 1.5249 + // non-breaking start and non-breaking end:" 1.5250 + } else { 1.5251 + buffer += 1.5252 + !nonBreakingStart && !nonBreakingEnd ? "\xa0 " 1.5253 + : nonBreakingStart && !nonBreakingEnd ? "\xa0 " 1.5254 + : !nonBreakingStart && nonBreakingEnd ? " \xa0" 1.5255 + : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" 1.5256 + : "impossible"; 1.5257 + } 1.5258 + 1.5259 + // "Return buffer." 1.5260 + return buffer; 1.5261 +} 1.5262 + 1.5263 +function canonicalizeWhitespace(node, offset, fixCollapsedSpace) { 1.5264 + if (fixCollapsedSpace === undefined) { 1.5265 + // "an optional boolean argument fix collapsed space that defaults to 1.5266 + // true" 1.5267 + fixCollapsedSpace = true; 1.5268 + } 1.5269 + 1.5270 + // "If node is neither editable nor an editing host, abort these steps." 1.5271 + if (!isEditable(node) && !isEditingHost(node)) { 1.5272 + return; 1.5273 + } 1.5274 + 1.5275 + // "Let start node equal node and let start offset equal offset." 1.5276 + var startNode = node; 1.5277 + var startOffset = offset; 1.5278 + 1.5279 + // "Repeat the following steps:" 1.5280 + while (true) { 1.5281 + // "If start node has a child in the same editing host with index start 1.5282 + // offset minus one, set start node to that child, then set start 1.5283 + // offset to start node's length." 1.5284 + if (0 <= startOffset - 1 1.5285 + && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) { 1.5286 + startNode = startNode.childNodes[startOffset - 1]; 1.5287 + startOffset = getNodeLength(startNode); 1.5288 + 1.5289 + // "Otherwise, if start offset is zero and start node does not follow a 1.5290 + // line break and start node's parent is in the same editing host, set 1.5291 + // start offset to start node's index, then set start node to its 1.5292 + // parent." 1.5293 + } else if (startOffset == 0 1.5294 + && !followsLineBreak(startNode) 1.5295 + && inSameEditingHost(startNode, startNode.parentNode)) { 1.5296 + startOffset = getNodeIndex(startNode); 1.5297 + startNode = startNode.parentNode; 1.5298 + 1.5299 + // "Otherwise, if start node is a Text node and its parent's resolved 1.5300 + // value for "white-space" is neither "pre" nor "pre-wrap" and start 1.5301 + // offset is not zero and the (start offset − 1)st element of start 1.5302 + // node's data is a space (0x0020) or non-breaking space (0x00A0), 1.5303 + // subtract one from start offset." 1.5304 + } else if (startNode.nodeType == Node.TEXT_NODE 1.5305 + && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1 1.5306 + && startOffset != 0 1.5307 + && /[ \xa0]/.test(startNode.data[startOffset - 1])) { 1.5308 + startOffset--; 1.5309 + 1.5310 + // "Otherwise, break from this loop." 1.5311 + } else { 1.5312 + break; 1.5313 + } 1.5314 + } 1.5315 + 1.5316 + // "Let end node equal start node and end offset equal start offset." 1.5317 + var endNode = startNode; 1.5318 + var endOffset = startOffset; 1.5319 + 1.5320 + // "Let length equal zero." 1.5321 + var length = 0; 1.5322 + 1.5323 + // "Let collapse spaces be true if start offset is zero and start node 1.5324 + // follows a line break, otherwise false." 1.5325 + var collapseSpaces = startOffset == 0 && followsLineBreak(startNode); 1.5326 + 1.5327 + // "Repeat the following steps:" 1.5328 + while (true) { 1.5329 + // "If end node has a child in the same editing host with index end 1.5330 + // offset, set end node to that child, then set end offset to zero." 1.5331 + if (endOffset < endNode.childNodes.length 1.5332 + && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { 1.5333 + endNode = endNode.childNodes[endOffset]; 1.5334 + endOffset = 0; 1.5335 + 1.5336 + // "Otherwise, if end offset is end node's length and end node does not 1.5337 + // precede a line break and end node's parent is in the same editing 1.5338 + // host, set end offset to one plus end node's index, then set end node 1.5339 + // to its parent." 1.5340 + } else if (endOffset == getNodeLength(endNode) 1.5341 + && !precedesLineBreak(endNode) 1.5342 + && inSameEditingHost(endNode, endNode.parentNode)) { 1.5343 + endOffset = 1 + getNodeIndex(endNode); 1.5344 + endNode = endNode.parentNode; 1.5345 + 1.5346 + // "Otherwise, if end node is a Text node and its parent's resolved 1.5347 + // value for "white-space" is neither "pre" nor "pre-wrap" and end 1.5348 + // offset is not end node's length and the end offsetth element of 1.5349 + // end node's data is a space (0x0020) or non-breaking space (0x00A0):" 1.5350 + } else if (endNode.nodeType == Node.TEXT_NODE 1.5351 + && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 1.5352 + && endOffset != getNodeLength(endNode) 1.5353 + && /[ \xa0]/.test(endNode.data[endOffset])) { 1.5354 + // "If fix collapsed space is true, and collapse spaces is true, 1.5355 + // and the end offsetth code unit of end node's data is a space 1.5356 + // (0x0020): call deleteData(end offset, 1) on end node, then 1.5357 + // continue this loop from the beginning." 1.5358 + if (fixCollapsedSpace 1.5359 + && collapseSpaces 1.5360 + && " " == endNode.data[endOffset]) { 1.5361 + endNode.deleteData(endOffset, 1); 1.5362 + continue; 1.5363 + } 1.5364 + 1.5365 + // "Set collapse spaces to true if the end offsetth element of end 1.5366 + // node's data is a space (0x0020), false otherwise." 1.5367 + collapseSpaces = " " == endNode.data[endOffset]; 1.5368 + 1.5369 + // "Add one to end offset." 1.5370 + endOffset++; 1.5371 + 1.5372 + // "Add one to length." 1.5373 + length++; 1.5374 + 1.5375 + // "Otherwise, break from this loop." 1.5376 + } else { 1.5377 + break; 1.5378 + } 1.5379 + } 1.5380 + 1.5381 + // "If fix collapsed space is true, then while (start node, start offset) 1.5382 + // is before (end node, end offset):" 1.5383 + if (fixCollapsedSpace) { 1.5384 + while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { 1.5385 + // "If end node has a child in the same editing host with index end 1.5386 + // offset − 1, set end node to that child, then set end offset to end 1.5387 + // node's length." 1.5388 + if (0 <= endOffset - 1 1.5389 + && endOffset - 1 < endNode.childNodes.length 1.5390 + && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) { 1.5391 + endNode = endNode.childNodes[endOffset - 1]; 1.5392 + endOffset = getNodeLength(endNode); 1.5393 + 1.5394 + // "Otherwise, if end offset is zero and end node's parent is in the 1.5395 + // same editing host, set end offset to end node's index, then set end 1.5396 + // node to its parent." 1.5397 + } else if (endOffset == 0 1.5398 + && inSameEditingHost(endNode, endNode.parentNode)) { 1.5399 + endOffset = getNodeIndex(endNode); 1.5400 + endNode = endNode.parentNode; 1.5401 + 1.5402 + // "Otherwise, if end node is a Text node and its parent's resolved 1.5403 + // value for "white-space" is neither "pre" nor "pre-wrap" and end 1.5404 + // offset is end node's length and the last code unit of end node's 1.5405 + // data is a space (0x0020) and end node precedes a line break:" 1.5406 + } else if (endNode.nodeType == Node.TEXT_NODE 1.5407 + && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 1.5408 + && endOffset == getNodeLength(endNode) 1.5409 + && endNode.data[endNode.data.length - 1] == " " 1.5410 + && precedesLineBreak(endNode)) { 1.5411 + // "Subtract one from end offset." 1.5412 + endOffset--; 1.5413 + 1.5414 + // "Subtract one from length." 1.5415 + length--; 1.5416 + 1.5417 + // "Call deleteData(end offset, 1) on end node." 1.5418 + endNode.deleteData(endOffset, 1); 1.5419 + 1.5420 + // "Otherwise, break from this loop." 1.5421 + } else { 1.5422 + break; 1.5423 + } 1.5424 + } 1.5425 + } 1.5426 + 1.5427 + // "Let replacement whitespace be the canonical space sequence of length 1.5428 + // length. non-breaking start is true if start offset is zero and start 1.5429 + // node follows a line break, and false otherwise. non-breaking end is true 1.5430 + // if end offset is end node's length and end node precedes a line break, 1.5431 + // and false otherwise." 1.5432 + var replacementWhitespace = canonicalSpaceSequence(length, 1.5433 + startOffset == 0 && followsLineBreak(startNode), 1.5434 + endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); 1.5435 + 1.5436 + // "While (start node, start offset) is before (end node, end offset):" 1.5437 + while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { 1.5438 + // "If start node has a child with index start offset, set start node 1.5439 + // to that child, then set start offset to zero." 1.5440 + if (startOffset < startNode.childNodes.length) { 1.5441 + startNode = startNode.childNodes[startOffset]; 1.5442 + startOffset = 0; 1.5443 + 1.5444 + // "Otherwise, if start node is not a Text node or if start offset is 1.5445 + // start node's length, set start offset to one plus start node's 1.5446 + // index, then set start node to its parent." 1.5447 + } else if (startNode.nodeType != Node.TEXT_NODE 1.5448 + || startOffset == getNodeLength(startNode)) { 1.5449 + startOffset = 1 + getNodeIndex(startNode); 1.5450 + startNode = startNode.parentNode; 1.5451 + 1.5452 + // "Otherwise:" 1.5453 + } else { 1.5454 + // "Remove the first element from replacement whitespace, and let 1.5455 + // element be that element." 1.5456 + var element = replacementWhitespace[0]; 1.5457 + replacementWhitespace = replacementWhitespace.slice(1); 1.5458 + 1.5459 + // "If element is not the same as the start offsetth element of 1.5460 + // start node's data:" 1.5461 + if (element != startNode.data[startOffset]) { 1.5462 + // "Call insertData(start offset, element) on start node." 1.5463 + startNode.insertData(startOffset, element); 1.5464 + 1.5465 + // "Call deleteData(start offset + 1, 1) on start node." 1.5466 + startNode.deleteData(startOffset + 1, 1); 1.5467 + } 1.5468 + 1.5469 + // "Add one to start offset." 1.5470 + startOffset++; 1.5471 + } 1.5472 + } 1.5473 +} 1.5474 + 1.5475 + 1.5476 +//@} 1.5477 +///// Indenting and outdenting ///// 1.5478 +//@{ 1.5479 + 1.5480 +function indentNodes(nodeList) { 1.5481 + // "If node list is empty, do nothing and abort these steps." 1.5482 + if (!nodeList.length) { 1.5483 + return; 1.5484 + } 1.5485 + 1.5486 + // "Let first node be the first member of node list." 1.5487 + var firstNode = nodeList[0]; 1.5488 + 1.5489 + // "If first node's parent is an ol or ul:" 1.5490 + if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) { 1.5491 + // "Let tag be the local name of the parent of first node." 1.5492 + var tag = firstNode.parentNode.tagName; 1.5493 + 1.5494 + // "Wrap node list, with sibling criteria returning true for an HTML 1.5495 + // element with local name tag and false otherwise, and new parent 1.5496 + // instructions returning the result of calling createElement(tag) on 1.5497 + // the ownerDocument of first node." 1.5498 + wrap(nodeList, 1.5499 + function(node) { return isHtmlElement(node, tag) }, 1.5500 + function() { return firstNode.ownerDocument.createElement(tag) }); 1.5501 + 1.5502 + // "Abort these steps." 1.5503 + return; 1.5504 + } 1.5505 + 1.5506 + // "Wrap node list, with sibling criteria returning true for a simple 1.5507 + // indentation element and false otherwise, and new parent instructions 1.5508 + // returning the result of calling createElement("blockquote") on the 1.5509 + // ownerDocument of first node. Let new parent be the result." 1.5510 + var newParent = wrap(nodeList, 1.5511 + function(node) { return isSimpleIndentationElement(node) }, 1.5512 + function() { return firstNode.ownerDocument.createElement("blockquote") }); 1.5513 + 1.5514 + // "Fix disallowed ancestors of new parent." 1.5515 + fixDisallowedAncestors(newParent); 1.5516 +} 1.5517 + 1.5518 +function outdentNode(node) { 1.5519 + // "If node is not editable, abort these steps." 1.5520 + if (!isEditable(node)) { 1.5521 + return; 1.5522 + } 1.5523 + 1.5524 + // "If node is a simple indentation element, remove node, preserving its 1.5525 + // descendants. Then abort these steps." 1.5526 + if (isSimpleIndentationElement(node)) { 1.5527 + removePreservingDescendants(node); 1.5528 + return; 1.5529 + } 1.5530 + 1.5531 + // "If node is an indentation element:" 1.5532 + if (isIndentationElement(node)) { 1.5533 + // "Unset the dir attribute of node, if any." 1.5534 + node.removeAttribute("dir"); 1.5535 + 1.5536 + // "Unset the margin, padding, and border CSS properties of node." 1.5537 + node.style.margin = ""; 1.5538 + node.style.padding = ""; 1.5539 + node.style.border = ""; 1.5540 + if (node.getAttribute("style") == "" 1.5541 + // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551 1.5542 + || node.getAttribute("style") == "border-width: initial; border-color: initial; ") { 1.5543 + node.removeAttribute("style"); 1.5544 + } 1.5545 + 1.5546 + // "Set the tag name of node to "div"." 1.5547 + setTagName(node, "div"); 1.5548 + 1.5549 + // "Abort these steps." 1.5550 + return; 1.5551 + } 1.5552 + 1.5553 + // "Let current ancestor be node's parent." 1.5554 + var currentAncestor = node.parentNode; 1.5555 + 1.5556 + // "Let ancestor list be a list of nodes, initially empty." 1.5557 + var ancestorList = []; 1.5558 + 1.5559 + // "While current ancestor is an editable Element that is neither a simple 1.5560 + // indentation element nor an ol nor a ul, append current ancestor to 1.5561 + // ancestor list and then set current ancestor to its parent." 1.5562 + while (isEditable(currentAncestor) 1.5563 + && currentAncestor.nodeType == Node.ELEMENT_NODE 1.5564 + && !isSimpleIndentationElement(currentAncestor) 1.5565 + && !isHtmlElement(currentAncestor, ["ol", "ul"])) { 1.5566 + ancestorList.push(currentAncestor); 1.5567 + currentAncestor = currentAncestor.parentNode; 1.5568 + } 1.5569 + 1.5570 + // "If current ancestor is not an editable simple indentation element:" 1.5571 + if (!isEditable(currentAncestor) 1.5572 + || !isSimpleIndentationElement(currentAncestor)) { 1.5573 + // "Let current ancestor be node's parent." 1.5574 + currentAncestor = node.parentNode; 1.5575 + 1.5576 + // "Let ancestor list be the empty list." 1.5577 + ancestorList = []; 1.5578 + 1.5579 + // "While current ancestor is an editable Element that is neither an 1.5580 + // indentation element nor an ol nor a ul, append current ancestor to 1.5581 + // ancestor list and then set current ancestor to its parent." 1.5582 + while (isEditable(currentAncestor) 1.5583 + && currentAncestor.nodeType == Node.ELEMENT_NODE 1.5584 + && !isIndentationElement(currentAncestor) 1.5585 + && !isHtmlElement(currentAncestor, ["ol", "ul"])) { 1.5586 + ancestorList.push(currentAncestor); 1.5587 + currentAncestor = currentAncestor.parentNode; 1.5588 + } 1.5589 + } 1.5590 + 1.5591 + // "If node is an ol or ul and current ancestor is not an editable 1.5592 + // indentation element:" 1.5593 + if (isHtmlElement(node, ["OL", "UL"]) 1.5594 + && (!isEditable(currentAncestor) 1.5595 + || !isIndentationElement(currentAncestor))) { 1.5596 + // "Unset the reversed, start, and type attributes of node, if any are 1.5597 + // set." 1.5598 + node.removeAttribute("reversed"); 1.5599 + node.removeAttribute("start"); 1.5600 + node.removeAttribute("type"); 1.5601 + 1.5602 + // "Let children be the children of node." 1.5603 + var children = [].slice.call(node.childNodes); 1.5604 + 1.5605 + // "If node has attributes, and its parent is not an ol or ul, set the 1.5606 + // tag name of node to "div"." 1.5607 + if (node.attributes.length 1.5608 + && !isHtmlElement(node.parentNode, ["OL", "UL"])) { 1.5609 + setTagName(node, "div"); 1.5610 + 1.5611 + // "Otherwise:" 1.5612 + } else { 1.5613 + // "Record the values of node's children, and let values be the 1.5614 + // result." 1.5615 + var values = recordValues([].slice.call(node.childNodes)); 1.5616 + 1.5617 + // "Remove node, preserving its descendants." 1.5618 + removePreservingDescendants(node); 1.5619 + 1.5620 + // "Restore the values from values." 1.5621 + restoreValues(values); 1.5622 + } 1.5623 + 1.5624 + // "Fix disallowed ancestors of each member of children." 1.5625 + for (var i = 0; i < children.length; i++) { 1.5626 + fixDisallowedAncestors(children[i]); 1.5627 + } 1.5628 + 1.5629 + // "Abort these steps." 1.5630 + return; 1.5631 + } 1.5632 + 1.5633 + // "If current ancestor is not an editable indentation element, abort these 1.5634 + // steps." 1.5635 + if (!isEditable(currentAncestor) 1.5636 + || !isIndentationElement(currentAncestor)) { 1.5637 + return; 1.5638 + } 1.5639 + 1.5640 + // "Append current ancestor to ancestor list." 1.5641 + ancestorList.push(currentAncestor); 1.5642 + 1.5643 + // "Let original ancestor be current ancestor." 1.5644 + var originalAncestor = currentAncestor; 1.5645 + 1.5646 + // "While ancestor list is not empty:" 1.5647 + while (ancestorList.length) { 1.5648 + // "Let current ancestor be the last member of ancestor list." 1.5649 + // 1.5650 + // "Remove the last member of ancestor list." 1.5651 + currentAncestor = ancestorList.pop(); 1.5652 + 1.5653 + // "Let target be the child of current ancestor that is equal to either 1.5654 + // node or the last member of ancestor list." 1.5655 + var target = node.parentNode == currentAncestor 1.5656 + ? node 1.5657 + : ancestorList[ancestorList.length - 1]; 1.5658 + 1.5659 + // "If target is an inline node that is not a br, and its nextSibling 1.5660 + // is a br, remove target's nextSibling from its parent." 1.5661 + if (isInlineNode(target) 1.5662 + && !isHtmlElement(target, "BR") 1.5663 + && isHtmlElement(target.nextSibling, "BR")) { 1.5664 + target.parentNode.removeChild(target.nextSibling); 1.5665 + } 1.5666 + 1.5667 + // "Let preceding siblings be the preceding siblings of target, and let 1.5668 + // following siblings be the following siblings of target." 1.5669 + var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target)); 1.5670 + var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target)); 1.5671 + 1.5672 + // "Indent preceding siblings." 1.5673 + indentNodes(precedingSiblings); 1.5674 + 1.5675 + // "Indent following siblings." 1.5676 + indentNodes(followingSiblings); 1.5677 + } 1.5678 + 1.5679 + // "Outdent original ancestor." 1.5680 + outdentNode(originalAncestor); 1.5681 +} 1.5682 + 1.5683 + 1.5684 +//@} 1.5685 +///// Toggling lists ///// 1.5686 +//@{ 1.5687 + 1.5688 +function toggleLists(tagName) { 1.5689 + // "Let mode be "disable" if the selection's list state is tag name, and 1.5690 + // "enable" otherwise." 1.5691 + var mode = getSelectionListState() == tagName ? "disable" : "enable"; 1.5692 + 1.5693 + var range = getActiveRange(); 1.5694 + tagName = tagName.toUpperCase(); 1.5695 + 1.5696 + // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is 1.5697 + // "ol"." 1.5698 + var otherTagName = tagName == "OL" ? "UL" : "OL"; 1.5699 + 1.5700 + // "Let items be a list of all lis that are ancestor containers of the 1.5701 + // range's start and/or end node." 1.5702 + // 1.5703 + // It's annoying to get this in tree order using functional stuff without 1.5704 + // doing getDescendants(document), which is slow, so I do it imperatively. 1.5705 + var items = []; 1.5706 + (function(){ 1.5707 + for ( 1.5708 + var ancestorContainer = range.endContainer; 1.5709 + ancestorContainer != range.commonAncestorContainer; 1.5710 + ancestorContainer = ancestorContainer.parentNode 1.5711 + ) { 1.5712 + if (isHtmlElement(ancestorContainer, "li")) { 1.5713 + items.unshift(ancestorContainer); 1.5714 + } 1.5715 + } 1.5716 + for ( 1.5717 + var ancestorContainer = range.startContainer; 1.5718 + ancestorContainer; 1.5719 + ancestorContainer = ancestorContainer.parentNode 1.5720 + ) { 1.5721 + if (isHtmlElement(ancestorContainer, "li")) { 1.5722 + items.unshift(ancestorContainer); 1.5723 + } 1.5724 + } 1.5725 + })(); 1.5726 + 1.5727 + // "For each item in items, normalize sublists of item." 1.5728 + items.forEach(normalizeSublists); 1.5729 + 1.5730 + // "Block-extend the range, and let new range be the result." 1.5731 + var newRange = blockExtend(range); 1.5732 + 1.5733 + // "If mode is "enable", then let lists to convert consist of every 1.5734 + // editable HTML element with local name other tag name that is contained 1.5735 + // in new range, and for every list in lists to convert:" 1.5736 + if (mode == "enable") { 1.5737 + getAllContainedNodes(newRange, function(node) { 1.5738 + return isEditable(node) 1.5739 + && isHtmlElement(node, otherTagName); 1.5740 + }).forEach(function(list) { 1.5741 + // "If list's previousSibling or nextSibling is an editable HTML 1.5742 + // element with local name tag name:" 1.5743 + if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName)) 1.5744 + || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) { 1.5745 + // "Let children be list's children." 1.5746 + var children = [].slice.call(list.childNodes); 1.5747 + 1.5748 + // "Record the values of children, and let values be the 1.5749 + // result." 1.5750 + var values = recordValues(children); 1.5751 + 1.5752 + // "Split the parent of children." 1.5753 + splitParent(children); 1.5754 + 1.5755 + // "Wrap children, with sibling criteria returning true for an 1.5756 + // HTML element with local name tag name and false otherwise." 1.5757 + wrap(children, function(node) { return isHtmlElement(node, tagName) }); 1.5758 + 1.5759 + // "Restore the values from values." 1.5760 + restoreValues(values); 1.5761 + 1.5762 + // "Otherwise, set the tag name of list to tag name." 1.5763 + } else { 1.5764 + setTagName(list, tagName); 1.5765 + } 1.5766 + }); 1.5767 + } 1.5768 + 1.5769 + // "Let node list be a list of nodes, initially empty." 1.5770 + // 1.5771 + // "For each node node contained in new range, if node is editable; the 1.5772 + // last member of node list (if any) is not an ancestor of node; node 1.5773 + // is not an indentation element; and either node is an ol or ul, or its 1.5774 + // parent is an ol or ul, or it is an allowed child of "li"; then append 1.5775 + // node to node list." 1.5776 + var nodeList = getContainedNodes(newRange, function(node) { 1.5777 + return isEditable(node) 1.5778 + && !isIndentationElement(node) 1.5779 + && (isHtmlElement(node, ["OL", "UL"]) 1.5780 + || isHtmlElement(node.parentNode, ["OL", "UL"]) 1.5781 + || isAllowedChild(node, "li")); 1.5782 + }); 1.5783 + 1.5784 + // "If mode is "enable", remove from node list any ol or ul whose parent is 1.5785 + // not also an ol or ul." 1.5786 + if (mode == "enable") { 1.5787 + nodeList = nodeList.filter(function(node) { 1.5788 + return !isHtmlElement(node, ["ol", "ul"]) 1.5789 + || isHtmlElement(node.parentNode, ["ol", "ul"]); 1.5790 + }); 1.5791 + } 1.5792 + 1.5793 + // "If mode is "disable", then while node list is not empty:" 1.5794 + if (mode == "disable") { 1.5795 + while (nodeList.length) { 1.5796 + // "Let sublist be an empty list of nodes." 1.5797 + var sublist = []; 1.5798 + 1.5799 + // "Remove the first member from node list and append it to 1.5800 + // sublist." 1.5801 + sublist.push(nodeList.shift()); 1.5802 + 1.5803 + // "If the first member of sublist is an HTML element with local 1.5804 + // name tag name, outdent it and continue this loop from the 1.5805 + // beginning." 1.5806 + if (isHtmlElement(sublist[0], tagName)) { 1.5807 + outdentNode(sublist[0]); 1.5808 + continue; 1.5809 + } 1.5810 + 1.5811 + // "While node list is not empty, and the first member of node list 1.5812 + // is the nextSibling of the last member of sublist and is not an 1.5813 + // HTML element with local name tag name, remove the first member 1.5814 + // from node list and append it to sublist." 1.5815 + while (nodeList.length 1.5816 + && nodeList[0] == sublist[sublist.length - 1].nextSibling 1.5817 + && !isHtmlElement(nodeList[0], tagName)) { 1.5818 + sublist.push(nodeList.shift()); 1.5819 + } 1.5820 + 1.5821 + // "Record the values of sublist, and let values be the result." 1.5822 + var values = recordValues(sublist); 1.5823 + 1.5824 + // "Split the parent of sublist." 1.5825 + splitParent(sublist); 1.5826 + 1.5827 + // "Fix disallowed ancestors of each member of sublist." 1.5828 + for (var i = 0; i < sublist.length; i++) { 1.5829 + fixDisallowedAncestors(sublist[i]); 1.5830 + } 1.5831 + 1.5832 + // "Restore the values from values." 1.5833 + restoreValues(values); 1.5834 + } 1.5835 + 1.5836 + // "Otherwise, while node list is not empty:" 1.5837 + } else { 1.5838 + while (nodeList.length) { 1.5839 + // "Let sublist be an empty list of nodes." 1.5840 + var sublist = []; 1.5841 + 1.5842 + // "While either sublist is empty, or node list is not empty and 1.5843 + // its first member is the nextSibling of sublist's last member:" 1.5844 + while (!sublist.length 1.5845 + || (nodeList.length 1.5846 + && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { 1.5847 + // "If node list's first member is a p or div, set the tag name 1.5848 + // of node list's first member to "li", and append the result 1.5849 + // to sublist. Remove the first member from node list." 1.5850 + if (isHtmlElement(nodeList[0], ["p", "div"])) { 1.5851 + sublist.push(setTagName(nodeList[0], "li")); 1.5852 + nodeList.shift(); 1.5853 + 1.5854 + // "Otherwise, if the first member of node list is an li or ol 1.5855 + // or ul, remove it from node list and append it to sublist." 1.5856 + } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) { 1.5857 + sublist.push(nodeList.shift()); 1.5858 + 1.5859 + // "Otherwise:" 1.5860 + } else { 1.5861 + // "Let nodes to wrap be a list of nodes, initially empty." 1.5862 + var nodesToWrap = []; 1.5863 + 1.5864 + // "While nodes to wrap is empty, or node list is not empty 1.5865 + // and its first member is the nextSibling of nodes to 1.5866 + // wrap's last member and the first member of node list is 1.5867 + // an inline node and the last member of nodes to wrap is 1.5868 + // an inline node other than a br, remove the first member 1.5869 + // from node list and append it to nodes to wrap." 1.5870 + while (!nodesToWrap.length 1.5871 + || (nodeList.length 1.5872 + && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling 1.5873 + && isInlineNode(nodeList[0]) 1.5874 + && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) 1.5875 + && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) { 1.5876 + nodesToWrap.push(nodeList.shift()); 1.5877 + } 1.5878 + 1.5879 + // "Wrap nodes to wrap, with new parent instructions 1.5880 + // returning the result of calling createElement("li") on 1.5881 + // the context object. Append the result to sublist." 1.5882 + sublist.push(wrap(nodesToWrap, 1.5883 + undefined, 1.5884 + function() { return document.createElement("li") })); 1.5885 + } 1.5886 + } 1.5887 + 1.5888 + // "If sublist's first member's parent is an HTML element with 1.5889 + // local name tag name, or if every member of sublist is an ol or 1.5890 + // ul, continue this loop from the beginning." 1.5891 + if (isHtmlElement(sublist[0].parentNode, tagName) 1.5892 + || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) { 1.5893 + continue; 1.5894 + } 1.5895 + 1.5896 + // "If sublist's first member's parent is an HTML element with 1.5897 + // local name other tag name:" 1.5898 + if (isHtmlElement(sublist[0].parentNode, otherTagName)) { 1.5899 + // "Record the values of sublist, and let values be the 1.5900 + // result." 1.5901 + var values = recordValues(sublist); 1.5902 + 1.5903 + // "Split the parent of sublist." 1.5904 + splitParent(sublist); 1.5905 + 1.5906 + // "Wrap sublist, with sibling criteria returning true for an 1.5907 + // HTML element with local name tag name and false otherwise, 1.5908 + // and new parent instructions returning the result of calling 1.5909 + // createElement(tag name) on the context object." 1.5910 + wrap(sublist, 1.5911 + function(node) { return isHtmlElement(node, tagName) }, 1.5912 + function() { return document.createElement(tagName) }); 1.5913 + 1.5914 + // "Restore the values from values." 1.5915 + restoreValues(values); 1.5916 + 1.5917 + // "Continue this loop from the beginning." 1.5918 + continue; 1.5919 + } 1.5920 + 1.5921 + // "Wrap sublist, with sibling criteria returning true for an HTML 1.5922 + // element with local name tag name and false otherwise, and new 1.5923 + // parent instructions being the following:" 1.5924 + // . . . 1.5925 + // "Fix disallowed ancestors of the previous step's result." 1.5926 + fixDisallowedAncestors(wrap(sublist, 1.5927 + function(node) { return isHtmlElement(node, tagName) }, 1.5928 + function() { 1.5929 + // "If sublist's first member's parent is not an editable 1.5930 + // simple indentation element, or sublist's first member's 1.5931 + // parent's previousSibling is not an editable HTML element 1.5932 + // with local name tag name, call createElement(tag name) 1.5933 + // on the context object and return the result." 1.5934 + if (!isEditable(sublist[0].parentNode) 1.5935 + || !isSimpleIndentationElement(sublist[0].parentNode) 1.5936 + || !isEditable(sublist[0].parentNode.previousSibling) 1.5937 + || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) { 1.5938 + return document.createElement(tagName); 1.5939 + } 1.5940 + 1.5941 + // "Let list be sublist's first member's parent's 1.5942 + // previousSibling." 1.5943 + var list = sublist[0].parentNode.previousSibling; 1.5944 + 1.5945 + // "Normalize sublists of list's lastChild." 1.5946 + normalizeSublists(list.lastChild); 1.5947 + 1.5948 + // "If list's lastChild is not an editable HTML element 1.5949 + // with local name tag name, call createElement(tag name) 1.5950 + // on the context object, and append the result as the last 1.5951 + // child of list." 1.5952 + if (!isEditable(list.lastChild) 1.5953 + || !isHtmlElement(list.lastChild, tagName)) { 1.5954 + list.appendChild(document.createElement(tagName)); 1.5955 + } 1.5956 + 1.5957 + // "Return the last child of list." 1.5958 + return list.lastChild; 1.5959 + } 1.5960 + )); 1.5961 + } 1.5962 + } 1.5963 +} 1.5964 + 1.5965 + 1.5966 +//@} 1.5967 +///// Justifying the selection ///// 1.5968 +//@{ 1.5969 + 1.5970 +function justifySelection(alignment) { 1.5971 + // "Block-extend the active range, and let new range be the result." 1.5972 + var newRange = blockExtend(globalRange); 1.5973 + 1.5974 + // "Let element list be a list of all editable Elements contained in new 1.5975 + // range that either has an attribute in the HTML namespace whose local 1.5976 + // name is "align", or has a style attribute that sets "text-align", or is 1.5977 + // a center." 1.5978 + var elementList = getAllContainedNodes(newRange, function(node) { 1.5979 + return node.nodeType == Node.ELEMENT_NODE 1.5980 + && isEditable(node) 1.5981 + // Ignoring namespaces here 1.5982 + && ( 1.5983 + node.hasAttribute("align") 1.5984 + || node.style.textAlign != "" 1.5985 + || isHtmlElement(node, "center") 1.5986 + ); 1.5987 + }); 1.5988 + 1.5989 + // "For each element in element list:" 1.5990 + for (var i = 0; i < elementList.length; i++) { 1.5991 + var element = elementList[i]; 1.5992 + 1.5993 + // "If element has an attribute in the HTML namespace whose local name 1.5994 + // is "align", remove that attribute." 1.5995 + element.removeAttribute("align"); 1.5996 + 1.5997 + // "Unset the CSS property "text-align" on element, if it's set by a 1.5998 + // style attribute." 1.5999 + element.style.textAlign = ""; 1.6000 + if (element.getAttribute("style") == "") { 1.6001 + element.removeAttribute("style"); 1.6002 + } 1.6003 + 1.6004 + // "If element is a div or span or center with no attributes, remove 1.6005 + // it, preserving its descendants." 1.6006 + if (isHtmlElement(element, ["div", "span", "center"]) 1.6007 + && !element.attributes.length) { 1.6008 + removePreservingDescendants(element); 1.6009 + } 1.6010 + 1.6011 + // "If element is a center with one or more attributes, set the tag 1.6012 + // name of element to "div"." 1.6013 + if (isHtmlElement(element, "center") 1.6014 + && element.attributes.length) { 1.6015 + setTagName(element, "div"); 1.6016 + } 1.6017 + } 1.6018 + 1.6019 + // "Block-extend the active range, and let new range be the result." 1.6020 + newRange = blockExtend(globalRange); 1.6021 + 1.6022 + // "Let node list be a list of nodes, initially empty." 1.6023 + var nodeList = []; 1.6024 + 1.6025 + // "For each node node contained in new range, append node to node list if 1.6026 + // the last member of node list (if any) is not an ancestor of node; node 1.6027 + // is editable; node is an allowed child of "div"; and node's alignment 1.6028 + // value is not alignment." 1.6029 + nodeList = getContainedNodes(newRange, function(node) { 1.6030 + return isEditable(node) 1.6031 + && isAllowedChild(node, "div") 1.6032 + && getAlignmentValue(node) != alignment; 1.6033 + }); 1.6034 + 1.6035 + // "While node list is not empty:" 1.6036 + while (nodeList.length) { 1.6037 + // "Let sublist be a list of nodes, initially empty." 1.6038 + var sublist = []; 1.6039 + 1.6040 + // "Remove the first member of node list and append it to sublist." 1.6041 + sublist.push(nodeList.shift()); 1.6042 + 1.6043 + // "While node list is not empty, and the first member of node list is 1.6044 + // the nextSibling of the last member of sublist, remove the first 1.6045 + // member of node list and append it to sublist." 1.6046 + while (nodeList.length 1.6047 + && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 1.6048 + sublist.push(nodeList.shift()); 1.6049 + } 1.6050 + 1.6051 + // "Wrap sublist. Sibling criteria returns true for any div that has 1.6052 + // one or both of the following two attributes and no other attributes, 1.6053 + // and false otherwise:" 1.6054 + // 1.6055 + // * "An align attribute whose value is an ASCII case-insensitive 1.6056 + // match for alignment. 1.6057 + // * "A style attribute which sets exactly one CSS property 1.6058 + // (including unrecognized or invalid attributes), which is 1.6059 + // "text-align", which is set to alignment. 1.6060 + // 1.6061 + // "New parent instructions are to call createElement("div") on the 1.6062 + // context object, then set its CSS property "text-align" to alignment 1.6063 + // and return the result." 1.6064 + wrap(sublist, 1.6065 + function(node) { 1.6066 + return isHtmlElement(node, "div") 1.6067 + && [].every.call(node.attributes, function(attr) { 1.6068 + return (attr.name == "align" && attr.value.toLowerCase() == alignment) 1.6069 + || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment); 1.6070 + }); 1.6071 + }, 1.6072 + function() { 1.6073 + var newParent = document.createElement("div"); 1.6074 + newParent.setAttribute("style", "text-align: " + alignment); 1.6075 + return newParent; 1.6076 + } 1.6077 + ); 1.6078 + } 1.6079 +} 1.6080 + 1.6081 + 1.6082 +//@} 1.6083 +///// Automatic linking ///// 1.6084 +//@{ 1.6085 +// "An autolinkable URL is a string of the following form:" 1.6086 +var autolinkableUrlRegexp = 1.6087 + // "Either a string matching the scheme pattern from RFC 3986 section 3.1 1.6088 + // followed by the literal string ://, or the literal string mailto:; 1.6089 + // followed by" 1.6090 + // 1.6091 + // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 1.6092 + "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)" 1.6093 + // "Zero or more characters other than space characters; followed by" 1.6094 + + "[^ \t\n\f\r]*" 1.6095 + // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}." 1.6096 + + "[^!\"'(),\\-.:;<>[\\]`{}]"; 1.6097 + 1.6098 +// "A valid e-mail address is a string that matches the ABNF production 1*( 1.6099 +// atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC 1.6100 +// 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5." 1.6101 +// 1.6102 +// atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / 1.6103 +// "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" 1.6104 +// 1.6105 +//<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> 1.6106 +//<let-dig-hyp> ::= <let-dig> | "-" 1.6107 +//<let-dig> ::= <letter> | <digit> 1.6108 +var validEmailRegexp = 1.6109 + "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*"; 1.6110 + 1.6111 +function autolink(node, endOffset) { 1.6112 + // "While (node, end offset)'s previous equivalent point is not null, set 1.6113 + // it to its previous equivalent point." 1.6114 + while (getPreviousEquivalentPoint(node, endOffset)) { 1.6115 + var prev = getPreviousEquivalentPoint(node, endOffset); 1.6116 + node = prev[0]; 1.6117 + endOffset = prev[1]; 1.6118 + } 1.6119 + 1.6120 + // "If node is not a Text node, or has an a ancestor, do nothing and abort 1.6121 + // these steps." 1.6122 + if (node.nodeType != Node.TEXT_NODE 1.6123 + || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) { 1.6124 + return; 1.6125 + } 1.6126 + 1.6127 + // "Let search be the largest substring of node's data whose end is end 1.6128 + // offset and that contains no space characters." 1.6129 + var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0]; 1.6130 + 1.6131 + // "If some substring of search is an autolinkable URL:" 1.6132 + if (new RegExp(autolinkableUrlRegexp).test(search)) { 1.6133 + // "While there is no substring of node's data ending at end offset 1.6134 + // that is an autolinkable URL, decrement end offset." 1.6135 + while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) { 1.6136 + endOffset--; 1.6137 + } 1.6138 + 1.6139 + // "Let start offset be the start index of the longest substring of 1.6140 + // node's data that is an autolinkable URL ending at end offset." 1.6141 + var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index; 1.6142 + 1.6143 + // "Let href be the substring of node's data starting at start offset 1.6144 + // and ending at end offset." 1.6145 + var href = node.substringData(startOffset, endOffset - startOffset); 1.6146 + 1.6147 + // "Otherwise, if some substring of search is a valid e-mail address:" 1.6148 + } else if (new RegExp(validEmailRegexp).test(search)) { 1.6149 + // "While there is no substring of node's data ending at end offset 1.6150 + // that is a valid e-mail address, decrement end offset." 1.6151 + while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) { 1.6152 + endOffset--; 1.6153 + } 1.6154 + 1.6155 + // "Let start offset be the start index of the longest substring of 1.6156 + // node's data that is a valid e-mail address ending at end offset." 1.6157 + var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index; 1.6158 + 1.6159 + // "Let href be "mailto:" concatenated with the substring of node's 1.6160 + // data starting at start offset and ending at end offset." 1.6161 + var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset); 1.6162 + 1.6163 + // "Otherwise, do nothing and abort these steps." 1.6164 + } else { 1.6165 + return; 1.6166 + } 1.6167 + 1.6168 + // "Let original range be the active range." 1.6169 + var originalRange = getActiveRange(); 1.6170 + 1.6171 + // "Create a new range with start (node, start offset) and end (node, end 1.6172 + // offset), and set the context object's selection's range to it." 1.6173 + var newRange = document.createRange(); 1.6174 + newRange.setStart(node, startOffset); 1.6175 + newRange.setEnd(node, endOffset); 1.6176 + getSelection().removeAllRanges(); 1.6177 + getSelection().addRange(newRange); 1.6178 + globalRange = newRange; 1.6179 + 1.6180 + // "Take the action for "createLink", with value equal to href." 1.6181 + commands.createlink.action(href); 1.6182 + 1.6183 + // "Set the context object's selection's range to original range." 1.6184 + getSelection().removeAllRanges(); 1.6185 + getSelection().addRange(originalRange); 1.6186 + globalRange = originalRange; 1.6187 +} 1.6188 +//@} 1.6189 +///// The delete command ///// 1.6190 +//@{ 1.6191 +commands["delete"] = { 1.6192 + preservesOverrides: true, 1.6193 + action: function() { 1.6194 + // "If the active range is not collapsed, delete the selection and 1.6195 + // return true." 1.6196 + if (!getActiveRange().collapsed) { 1.6197 + deleteSelection(); 1.6198 + return true; 1.6199 + } 1.6200 + 1.6201 + // "Canonicalize whitespace at the active range's start." 1.6202 + canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); 1.6203 + 1.6204 + // "Let node and offset be the active range's start node and offset." 1.6205 + var node = getActiveRange().startContainer; 1.6206 + var offset = getActiveRange().startOffset; 1.6207 + 1.6208 + // "Repeat the following steps:" 1.6209 + while (true) { 1.6210 + // "If offset is zero and node's previousSibling is an editable 1.6211 + // invisible node, remove node's previousSibling from its parent." 1.6212 + if (offset == 0 1.6213 + && isEditable(node.previousSibling) 1.6214 + && isInvisible(node.previousSibling)) { 1.6215 + node.parentNode.removeChild(node.previousSibling); 1.6216 + 1.6217 + // "Otherwise, if node has a child with index offset − 1 and that 1.6218 + // child is an editable invisible node, remove that child from 1.6219 + // node, then subtract one from offset." 1.6220 + } else if (0 <= offset - 1 1.6221 + && offset - 1 < node.childNodes.length 1.6222 + && isEditable(node.childNodes[offset - 1]) 1.6223 + && isInvisible(node.childNodes[offset - 1])) { 1.6224 + node.removeChild(node.childNodes[offset - 1]); 1.6225 + offset--; 1.6226 + 1.6227 + // "Otherwise, if offset is zero and node is an inline node, or if 1.6228 + // node is an invisible node, set offset to the index of node, then 1.6229 + // set node to its parent." 1.6230 + } else if ((offset == 0 1.6231 + && isInlineNode(node)) 1.6232 + || isInvisible(node)) { 1.6233 + offset = getNodeIndex(node); 1.6234 + node = node.parentNode; 1.6235 + 1.6236 + // "Otherwise, if node has a child with index offset − 1 and that 1.6237 + // child is an editable a, remove that child from node, preserving 1.6238 + // its descendants. Then return true." 1.6239 + } else if (0 <= offset - 1 1.6240 + && offset - 1 < node.childNodes.length 1.6241 + && isEditable(node.childNodes[offset - 1]) 1.6242 + && isHtmlElement(node.childNodes[offset - 1], "a")) { 1.6243 + removePreservingDescendants(node.childNodes[offset - 1]); 1.6244 + return true; 1.6245 + 1.6246 + // "Otherwise, if node has a child with index offset − 1 and that 1.6247 + // child is not a block node or a br or an img, set node to that 1.6248 + // child, then set offset to the length of node." 1.6249 + } else if (0 <= offset - 1 1.6250 + && offset - 1 < node.childNodes.length 1.6251 + && !isBlockNode(node.childNodes[offset - 1]) 1.6252 + && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) { 1.6253 + node = node.childNodes[offset - 1]; 1.6254 + offset = getNodeLength(node); 1.6255 + 1.6256 + // "Otherwise, break from this loop." 1.6257 + } else { 1.6258 + break; 1.6259 + } 1.6260 + } 1.6261 + 1.6262 + // "If node is a Text node and offset is not zero, or if node is a 1.6263 + // block node that has a child with index offset − 1 and that child is 1.6264 + // a br or hr or img:" 1.6265 + if ((node.nodeType == Node.TEXT_NODE 1.6266 + && offset != 0) 1.6267 + || (isBlockNode(node) 1.6268 + && 0 <= offset - 1 1.6269 + && offset - 1 < node.childNodes.length 1.6270 + && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) { 1.6271 + // "Call collapse(node, offset) on the context object's Selection." 1.6272 + getSelection().collapse(node, offset); 1.6273 + getActiveRange().setEnd(node, offset); 1.6274 + 1.6275 + // "Call extend(node, offset − 1) on the context object's 1.6276 + // Selection." 1.6277 + getSelection().extend(node, offset - 1); 1.6278 + getActiveRange().setStart(node, offset - 1); 1.6279 + 1.6280 + // "Delete the selection." 1.6281 + deleteSelection(); 1.6282 + 1.6283 + // "Return true." 1.6284 + return true; 1.6285 + } 1.6286 + 1.6287 + // "If node is an inline node, return true." 1.6288 + if (isInlineNode(node)) { 1.6289 + return true; 1.6290 + } 1.6291 + 1.6292 + // "If node is an li or dt or dd and is the first child of its parent, 1.6293 + // and offset is zero:" 1.6294 + if (isHtmlElement(node, ["li", "dt", "dd"]) 1.6295 + && node == node.parentNode.firstChild 1.6296 + && offset == 0) { 1.6297 + // "Let items be a list of all lis that are ancestors of node." 1.6298 + // 1.6299 + // Remember, must be in tree order. 1.6300 + var items = []; 1.6301 + for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) { 1.6302 + if (isHtmlElement(ancestor, "li")) { 1.6303 + items.unshift(ancestor); 1.6304 + } 1.6305 + } 1.6306 + 1.6307 + // "Normalize sublists of each item in items." 1.6308 + for (var i = 0; i < items.length; i++) { 1.6309 + normalizeSublists(items[i]); 1.6310 + } 1.6311 + 1.6312 + // "Record the values of the one-node list consisting of node, and 1.6313 + // let values be the result." 1.6314 + var values = recordValues([node]); 1.6315 + 1.6316 + // "Split the parent of the one-node list consisting of node." 1.6317 + splitParent([node]); 1.6318 + 1.6319 + // "Restore the values from values." 1.6320 + restoreValues(values); 1.6321 + 1.6322 + // "If node is a dd or dt, and it is not an allowed child of any of 1.6323 + // its ancestors in the same editing host, set the tag name of node 1.6324 + // to the default single-line container name and let node be the 1.6325 + // result." 1.6326 + if (isHtmlElement(node, ["dd", "dt"]) 1.6327 + && getAncestors(node).every(function(ancestor) { 1.6328 + return !inSameEditingHost(node, ancestor) 1.6329 + || !isAllowedChild(node, ancestor) 1.6330 + })) { 1.6331 + node = setTagName(node, defaultSingleLineContainerName); 1.6332 + } 1.6333 + 1.6334 + // "Fix disallowed ancestors of node." 1.6335 + fixDisallowedAncestors(node); 1.6336 + 1.6337 + // "Return true." 1.6338 + return true; 1.6339 + } 1.6340 + 1.6341 + // "Let start node equal node and let start offset equal offset." 1.6342 + var startNode = node; 1.6343 + var startOffset = offset; 1.6344 + 1.6345 + // "Repeat the following steps:" 1.6346 + while (true) { 1.6347 + // "If start offset is zero, set start offset to the index of start 1.6348 + // node and then set start node to its parent." 1.6349 + if (startOffset == 0) { 1.6350 + startOffset = getNodeIndex(startNode); 1.6351 + startNode = startNode.parentNode; 1.6352 + 1.6353 + // "Otherwise, if start node has an editable invisible child with 1.6354 + // index start offset minus one, remove it from start node and 1.6355 + // subtract one from start offset." 1.6356 + } else if (0 <= startOffset - 1 1.6357 + && startOffset - 1 < startNode.childNodes.length 1.6358 + && isEditable(startNode.childNodes[startOffset - 1]) 1.6359 + && isInvisible(startNode.childNodes[startOffset - 1])) { 1.6360 + startNode.removeChild(startNode.childNodes[startOffset - 1]); 1.6361 + startOffset--; 1.6362 + 1.6363 + // "Otherwise, break from this loop." 1.6364 + } else { 1.6365 + break; 1.6366 + } 1.6367 + } 1.6368 + 1.6369 + // "If offset is zero, and node has an editable ancestor container in 1.6370 + // the same editing host that's an indentation element:" 1.6371 + if (offset == 0 1.6372 + && getAncestors(node).concat(node).filter(function(ancestor) { 1.6373 + return isEditable(ancestor) 1.6374 + && inSameEditingHost(ancestor, node) 1.6375 + && isIndentationElement(ancestor); 1.6376 + }).length) { 1.6377 + // "Block-extend the range whose start and end are both (node, 0), 1.6378 + // and let new range be the result." 1.6379 + var newRange = document.createRange(); 1.6380 + newRange.setStart(node, 0); 1.6381 + newRange = blockExtend(newRange); 1.6382 + 1.6383 + // "Let node list be a list of nodes, initially empty." 1.6384 + // 1.6385 + // "For each node current node contained in new range, append 1.6386 + // current node to node list if the last member of node list (if 1.6387 + // any) is not an ancestor of current node, and current node is 1.6388 + // editable but has no editable descendants." 1.6389 + var nodeList = getContainedNodes(newRange, function(currentNode) { 1.6390 + return isEditable(currentNode) 1.6391 + && !hasEditableDescendants(currentNode); 1.6392 + }); 1.6393 + 1.6394 + // "Outdent each node in node list." 1.6395 + for (var i = 0; i < nodeList.length; i++) { 1.6396 + outdentNode(nodeList[i]); 1.6397 + } 1.6398 + 1.6399 + // "Return true." 1.6400 + return true; 1.6401 + } 1.6402 + 1.6403 + // "If the child of start node with index start offset is a table, 1.6404 + // return true." 1.6405 + if (isHtmlElement(startNode.childNodes[startOffset], "table")) { 1.6406 + return true; 1.6407 + } 1.6408 + 1.6409 + // "If start node has a child with index start offset − 1, and that 1.6410 + // child is a table:" 1.6411 + if (0 <= startOffset - 1 1.6412 + && startOffset - 1 < startNode.childNodes.length 1.6413 + && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) { 1.6414 + // "Call collapse(start node, start offset − 1) on the context 1.6415 + // object's Selection." 1.6416 + getSelection().collapse(startNode, startOffset - 1); 1.6417 + getActiveRange().setStart(startNode, startOffset - 1); 1.6418 + 1.6419 + // "Call extend(start node, start offset) on the context object's 1.6420 + // Selection." 1.6421 + getSelection().extend(startNode, startOffset); 1.6422 + getActiveRange().setEnd(startNode, startOffset); 1.6423 + 1.6424 + // "Return true." 1.6425 + return true; 1.6426 + } 1.6427 + 1.6428 + // "If offset is zero; and either the child of start node with index 1.6429 + // start offset minus one is an hr, or the child is a br whose 1.6430 + // previousSibling is either a br or not an inline node:" 1.6431 + if (offset == 0 1.6432 + && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr") 1.6433 + || ( 1.6434 + isHtmlElement(startNode.childNodes[startOffset - 1], "br") 1.6435 + && ( 1.6436 + isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br") 1.6437 + || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling) 1.6438 + ) 1.6439 + ) 1.6440 + )) { 1.6441 + // "Call collapse(start node, start offset − 1) on the context 1.6442 + // object's Selection." 1.6443 + getSelection().collapse(startNode, startOffset - 1); 1.6444 + getActiveRange().setStart(startNode, startOffset - 1); 1.6445 + 1.6446 + // "Call extend(start node, start offset) on the context object's 1.6447 + // Selection." 1.6448 + getSelection().extend(startNode, startOffset); 1.6449 + getActiveRange().setEnd(startNode, startOffset); 1.6450 + 1.6451 + // "Delete the selection." 1.6452 + deleteSelection(); 1.6453 + 1.6454 + // "Call collapse(node, offset) on the Selection." 1.6455 + getSelection().collapse(node, offset); 1.6456 + getActiveRange().setStart(node, offset); 1.6457 + getActiveRange().collapse(true); 1.6458 + 1.6459 + // "Return true." 1.6460 + return true; 1.6461 + } 1.6462 + 1.6463 + // "If the child of start node with index start offset is an li or dt 1.6464 + // or dd, and that child's firstChild is an inline node, and start 1.6465 + // offset is not zero:" 1.6466 + if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) 1.6467 + && isInlineNode(startNode.childNodes[startOffset].firstChild) 1.6468 + && startOffset != 0) { 1.6469 + // "Let previous item be the child of start node with index start 1.6470 + // offset minus one." 1.6471 + var previousItem = startNode.childNodes[startOffset - 1]; 1.6472 + 1.6473 + // "If previous item's lastChild is an inline node other than a br, 1.6474 + // call createElement("br") on the context object and append the 1.6475 + // result as the last child of previous item." 1.6476 + if (isInlineNode(previousItem.lastChild) 1.6477 + && !isHtmlElement(previousItem.lastChild, "br")) { 1.6478 + previousItem.appendChild(document.createElement("br")); 1.6479 + } 1.6480 + 1.6481 + // "If previous item's lastChild is an inline node, call 1.6482 + // createElement("br") on the context object and append the result 1.6483 + // as the last child of previous item." 1.6484 + if (isInlineNode(previousItem.lastChild)) { 1.6485 + previousItem.appendChild(document.createElement("br")); 1.6486 + } 1.6487 + } 1.6488 + 1.6489 + // "If start node's child with index start offset is an li or dt or dd, 1.6490 + // and that child's previousSibling is also an li or dt or dd:" 1.6491 + if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) 1.6492 + && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) { 1.6493 + // "Call cloneRange() on the active range, and let original range 1.6494 + // be the result." 1.6495 + // 1.6496 + // We need to add it to extraRanges so it will actually get updated 1.6497 + // when moving preserving ranges. 1.6498 + var originalRange = getActiveRange().cloneRange(); 1.6499 + extraRanges.push(originalRange); 1.6500 + 1.6501 + // "Set start node to its child with index start offset − 1." 1.6502 + startNode = startNode.childNodes[startOffset - 1]; 1.6503 + 1.6504 + // "Set start offset to start node's length." 1.6505 + startOffset = getNodeLength(startNode); 1.6506 + 1.6507 + // "Set node to start node's nextSibling." 1.6508 + node = startNode.nextSibling; 1.6509 + 1.6510 + // "Call collapse(start node, start offset) on the context object's 1.6511 + // Selection." 1.6512 + getSelection().collapse(startNode, startOffset); 1.6513 + getActiveRange().setStart(startNode, startOffset); 1.6514 + 1.6515 + // "Call extend(node, 0) on the context object's Selection." 1.6516 + getSelection().extend(node, 0); 1.6517 + getActiveRange().setEnd(node, 0); 1.6518 + 1.6519 + // "Delete the selection." 1.6520 + deleteSelection(); 1.6521 + 1.6522 + // "Call removeAllRanges() on the context object's Selection." 1.6523 + getSelection().removeAllRanges(); 1.6524 + 1.6525 + // "Call addRange(original range) on the context object's 1.6526 + // Selection." 1.6527 + getSelection().addRange(originalRange); 1.6528 + getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset); 1.6529 + getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset); 1.6530 + 1.6531 + // "Return true." 1.6532 + extraRanges.pop(); 1.6533 + return true; 1.6534 + } 1.6535 + 1.6536 + // "While start node has a child with index start offset minus one:" 1.6537 + while (0 <= startOffset - 1 1.6538 + && startOffset - 1 < startNode.childNodes.length) { 1.6539 + // "If start node's child with index start offset minus one is 1.6540 + // editable and invisible, remove it from start node, then subtract 1.6541 + // one from start offset." 1.6542 + if (isEditable(startNode.childNodes[startOffset - 1]) 1.6543 + && isInvisible(startNode.childNodes[startOffset - 1])) { 1.6544 + startNode.removeChild(startNode.childNodes[startOffset - 1]); 1.6545 + startOffset--; 1.6546 + 1.6547 + // "Otherwise, set start node to its child with index start offset 1.6548 + // minus one, then set start offset to the length of start node." 1.6549 + } else { 1.6550 + startNode = startNode.childNodes[startOffset - 1]; 1.6551 + startOffset = getNodeLength(startNode); 1.6552 + } 1.6553 + } 1.6554 + 1.6555 + // "Call collapse(start node, start offset) on the context object's 1.6556 + // Selection." 1.6557 + getSelection().collapse(startNode, startOffset); 1.6558 + getActiveRange().setStart(startNode, startOffset); 1.6559 + 1.6560 + // "Call extend(node, offset) on the context object's Selection." 1.6561 + getSelection().extend(node, offset); 1.6562 + getActiveRange().setEnd(node, offset); 1.6563 + 1.6564 + // "Delete the selection, with direction "backward"." 1.6565 + deleteSelection({direction: "backward"}); 1.6566 + 1.6567 + // "Return true." 1.6568 + return true; 1.6569 + } 1.6570 +}; 1.6571 + 1.6572 +//@} 1.6573 +///// The formatBlock command ///// 1.6574 +//@{ 1.6575 +// "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", 1.6576 +// "h4", "h5", "h6", "p", or "pre"." 1.6577 +var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", 1.6578 + "h4", "h5", "h6", "p", "pre"]; 1.6579 + 1.6580 +commands.formatblock = { 1.6581 + preservesOverrides: true, 1.6582 + action: function(value) { 1.6583 + // "If value begins with a "<" character and ends with a ">" character, 1.6584 + // remove the first and last characters from it." 1.6585 + if (/^<.*>$/.test(value)) { 1.6586 + value = value.slice(1, -1); 1.6587 + } 1.6588 + 1.6589 + // "Let value be converted to ASCII lowercase." 1.6590 + value = value.toLowerCase(); 1.6591 + 1.6592 + // "If value is not a formattable block name, return false." 1.6593 + if (formattableBlockNames.indexOf(value) == -1) { 1.6594 + return false; 1.6595 + } 1.6596 + 1.6597 + // "Block-extend the active range, and let new range be the result." 1.6598 + var newRange = blockExtend(getActiveRange()); 1.6599 + 1.6600 + // "Let node list be an empty list of nodes." 1.6601 + // 1.6602 + // "For each node node contained in new range, append node to node list 1.6603 + // if it is editable, the last member of original node list (if any) is 1.6604 + // not an ancestor of node, node is either a non-list single-line 1.6605 + // container or an allowed child of "p" or a dd or dt, and node is not 1.6606 + // the ancestor of a prohibited paragraph child." 1.6607 + var nodeList = getContainedNodes(newRange, function(node) { 1.6608 + return isEditable(node) 1.6609 + && (isNonListSingleLineContainer(node) 1.6610 + || isAllowedChild(node, "p") 1.6611 + || isHtmlElement(node, ["dd", "dt"])) 1.6612 + && !getDescendants(node).some(isProhibitedParagraphChild); 1.6613 + }); 1.6614 + 1.6615 + // "Record the values of node list, and let values be the result." 1.6616 + var values = recordValues(nodeList); 1.6617 + 1.6618 + // "For each node in node list, while node is the descendant of an 1.6619 + // editable HTML element in the same editing host, whose local name is 1.6620 + // a formattable block name, and which is not the ancestor of a 1.6621 + // prohibited paragraph child, split the parent of the one-node list 1.6622 + // consisting of node." 1.6623 + for (var i = 0; i < nodeList.length; i++) { 1.6624 + var node = nodeList[i]; 1.6625 + while (getAncestors(node).some(function(ancestor) { 1.6626 + return isEditable(ancestor) 1.6627 + && inSameEditingHost(ancestor, node) 1.6628 + && isHtmlElement(ancestor, formattableBlockNames) 1.6629 + && !getDescendants(ancestor).some(isProhibitedParagraphChild); 1.6630 + })) { 1.6631 + splitParent([node]); 1.6632 + } 1.6633 + } 1.6634 + 1.6635 + // "Restore the values from values." 1.6636 + restoreValues(values); 1.6637 + 1.6638 + // "While node list is not empty:" 1.6639 + while (nodeList.length) { 1.6640 + var sublist; 1.6641 + 1.6642 + // "If the first member of node list is a single-line 1.6643 + // container:" 1.6644 + if (isSingleLineContainer(nodeList[0])) { 1.6645 + // "Let sublist be the children of the first member of node 1.6646 + // list." 1.6647 + sublist = [].slice.call(nodeList[0].childNodes); 1.6648 + 1.6649 + // "Record the values of sublist, and let values be the 1.6650 + // result." 1.6651 + var values = recordValues(sublist); 1.6652 + 1.6653 + // "Remove the first member of node list from its parent, 1.6654 + // preserving its descendants." 1.6655 + removePreservingDescendants(nodeList[0]); 1.6656 + 1.6657 + // "Restore the values from values." 1.6658 + restoreValues(values); 1.6659 + 1.6660 + // "Remove the first member from node list." 1.6661 + nodeList.shift(); 1.6662 + 1.6663 + // "Otherwise:" 1.6664 + } else { 1.6665 + // "Let sublist be an empty list of nodes." 1.6666 + sublist = []; 1.6667 + 1.6668 + // "Remove the first member of node list and append it to 1.6669 + // sublist." 1.6670 + sublist.push(nodeList.shift()); 1.6671 + 1.6672 + // "While node list is not empty, and the first member of 1.6673 + // node list is the nextSibling of the last member of 1.6674 + // sublist, and the first member of node list is not a 1.6675 + // single-line container, and the last member of sublist is 1.6676 + // not a br, remove the first member of node list and 1.6677 + // append it to sublist." 1.6678 + while (nodeList.length 1.6679 + && nodeList[0] == sublist[sublist.length - 1].nextSibling 1.6680 + && !isSingleLineContainer(nodeList[0]) 1.6681 + && !isHtmlElement(sublist[sublist.length - 1], "BR")) { 1.6682 + sublist.push(nodeList.shift()); 1.6683 + } 1.6684 + } 1.6685 + 1.6686 + // "Wrap sublist. If value is "div" or "p", sibling criteria 1.6687 + // returns false; otherwise it returns true for an HTML element 1.6688 + // with local name value and no attributes, and false otherwise. 1.6689 + // New parent instructions return the result of running 1.6690 + // createElement(value) on the context object. Then fix disallowed 1.6691 + // ancestors of the result." 1.6692 + fixDisallowedAncestors(wrap(sublist, 1.6693 + ["div", "p"].indexOf(value) == - 1 1.6694 + ? function(node) { return isHtmlElement(node, value) && !node.attributes.length } 1.6695 + : function() { return false }, 1.6696 + function() { return document.createElement(value) })); 1.6697 + } 1.6698 + 1.6699 + // "Return true." 1.6700 + return true; 1.6701 + }, indeterm: function() { 1.6702 + // "If the active range is null, return false." 1.6703 + if (!getActiveRange()) { 1.6704 + return false; 1.6705 + } 1.6706 + 1.6707 + // "Block-extend the active range, and let new range be the result." 1.6708 + var newRange = blockExtend(getActiveRange()); 1.6709 + 1.6710 + // "Let node list be all visible editable nodes that are contained in 1.6711 + // new range and have no children." 1.6712 + var nodeList = getAllContainedNodes(newRange, function(node) { 1.6713 + return isVisible(node) 1.6714 + && isEditable(node) 1.6715 + && !node.hasChildNodes(); 1.6716 + }); 1.6717 + 1.6718 + // "If node list is empty, return false." 1.6719 + if (!nodeList.length) { 1.6720 + return false; 1.6721 + } 1.6722 + 1.6723 + // "Let type be null." 1.6724 + var type = null; 1.6725 + 1.6726 + // "For each node in node list:" 1.6727 + for (var i = 0; i < nodeList.length; i++) { 1.6728 + var node = nodeList[i]; 1.6729 + 1.6730 + // "While node's parent is editable and in the same editing host as 1.6731 + // node, and node is not an HTML element whose local name is a 1.6732 + // formattable block name, set node to its parent." 1.6733 + while (isEditable(node.parentNode) 1.6734 + && inSameEditingHost(node, node.parentNode) 1.6735 + && !isHtmlElement(node, formattableBlockNames)) { 1.6736 + node = node.parentNode; 1.6737 + } 1.6738 + 1.6739 + // "Let current type be the empty string." 1.6740 + var currentType = ""; 1.6741 + 1.6742 + // "If node is an editable HTML element whose local name is a 1.6743 + // formattable block name, and node is not the ancestor of a 1.6744 + // prohibited paragraph child, set current type to node's local 1.6745 + // name." 1.6746 + if (isEditable(node) 1.6747 + && isHtmlElement(node, formattableBlockNames) 1.6748 + && !getDescendants(node).some(isProhibitedParagraphChild)) { 1.6749 + currentType = node.tagName; 1.6750 + } 1.6751 + 1.6752 + // "If type is null, set type to current type." 1.6753 + if (type === null) { 1.6754 + type = currentType; 1.6755 + 1.6756 + // "Otherwise, if type does not equal current type, return true." 1.6757 + } else if (type != currentType) { 1.6758 + return true; 1.6759 + } 1.6760 + } 1.6761 + 1.6762 + // "Return false." 1.6763 + return false; 1.6764 + }, value: function() { 1.6765 + // "If the active range is null, return the empty string." 1.6766 + if (!getActiveRange()) { 1.6767 + return ""; 1.6768 + } 1.6769 + 1.6770 + // "Block-extend the active range, and let new range be the result." 1.6771 + var newRange = blockExtend(getActiveRange()); 1.6772 + 1.6773 + // "Let node be the first visible editable node that is contained in 1.6774 + // new range and has no children. If there is no such node, return the 1.6775 + // empty string." 1.6776 + var nodes = getAllContainedNodes(newRange, function(node) { 1.6777 + return isVisible(node) 1.6778 + && isEditable(node) 1.6779 + && !node.hasChildNodes(); 1.6780 + }); 1.6781 + if (!nodes.length) { 1.6782 + return ""; 1.6783 + } 1.6784 + var node = nodes[0]; 1.6785 + 1.6786 + // "While node's parent is editable and in the same editing host as 1.6787 + // node, and node is not an HTML element whose local name is a 1.6788 + // formattable block name, set node to its parent." 1.6789 + while (isEditable(node.parentNode) 1.6790 + && inSameEditingHost(node, node.parentNode) 1.6791 + && !isHtmlElement(node, formattableBlockNames)) { 1.6792 + node = node.parentNode; 1.6793 + } 1.6794 + 1.6795 + // "If node is an editable HTML element whose local name is a 1.6796 + // formattable block name, and node is not the ancestor of a prohibited 1.6797 + // paragraph child, return node's local name, converted to ASCII 1.6798 + // lowercase." 1.6799 + if (isEditable(node) 1.6800 + && isHtmlElement(node, formattableBlockNames) 1.6801 + && !getDescendants(node).some(isProhibitedParagraphChild)) { 1.6802 + return node.tagName.toLowerCase(); 1.6803 + } 1.6804 + 1.6805 + // "Return the empty string." 1.6806 + return ""; 1.6807 + } 1.6808 +}; 1.6809 + 1.6810 +//@} 1.6811 +///// The forwardDelete command ///// 1.6812 +//@{ 1.6813 +commands.forwarddelete = { 1.6814 + preservesOverrides: true, 1.6815 + action: function() { 1.6816 + // "If the active range is not collapsed, delete the selection and 1.6817 + // return true." 1.6818 + if (!getActiveRange().collapsed) { 1.6819 + deleteSelection(); 1.6820 + return true; 1.6821 + } 1.6822 + 1.6823 + // "Canonicalize whitespace at the active range's start." 1.6824 + canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); 1.6825 + 1.6826 + // "Let node and offset be the active range's start node and offset." 1.6827 + var node = getActiveRange().startContainer; 1.6828 + var offset = getActiveRange().startOffset; 1.6829 + 1.6830 + // "Repeat the following steps:" 1.6831 + while (true) { 1.6832 + // "If offset is the length of node and node's nextSibling is an 1.6833 + // editable invisible node, remove node's nextSibling from its 1.6834 + // parent." 1.6835 + if (offset == getNodeLength(node) 1.6836 + && isEditable(node.nextSibling) 1.6837 + && isInvisible(node.nextSibling)) { 1.6838 + node.parentNode.removeChild(node.nextSibling); 1.6839 + 1.6840 + // "Otherwise, if node has a child with index offset and that child 1.6841 + // is an editable invisible node, remove that child from node." 1.6842 + } else if (offset < node.childNodes.length 1.6843 + && isEditable(node.childNodes[offset]) 1.6844 + && isInvisible(node.childNodes[offset])) { 1.6845 + node.removeChild(node.childNodes[offset]); 1.6846 + 1.6847 + // "Otherwise, if offset is the length of node and node is an 1.6848 + // inline node, or if node is invisible, set offset to one plus the 1.6849 + // index of node, then set node to its parent." 1.6850 + } else if ((offset == getNodeLength(node) 1.6851 + && isInlineNode(node)) 1.6852 + || isInvisible(node)) { 1.6853 + offset = 1 + getNodeIndex(node); 1.6854 + node = node.parentNode; 1.6855 + 1.6856 + // "Otherwise, if node has a child with index offset and that child 1.6857 + // is neither a block node nor a br nor an img nor a collapsed 1.6858 + // block prop, set node to that child, then set offset to zero." 1.6859 + } else if (offset < node.childNodes.length 1.6860 + && !isBlockNode(node.childNodes[offset]) 1.6861 + && !isHtmlElement(node.childNodes[offset], ["br", "img"]) 1.6862 + && !isCollapsedBlockProp(node.childNodes[offset])) { 1.6863 + node = node.childNodes[offset]; 1.6864 + offset = 0; 1.6865 + 1.6866 + // "Otherwise, break from this loop." 1.6867 + } else { 1.6868 + break; 1.6869 + } 1.6870 + } 1.6871 + 1.6872 + // "If node is a Text node and offset is not node's length:" 1.6873 + if (node.nodeType == Node.TEXT_NODE 1.6874 + && offset != getNodeLength(node)) { 1.6875 + // "Let end offset be offset plus one." 1.6876 + var endOffset = offset + 1; 1.6877 + 1.6878 + // "While end offset is not node's length and the end offsetth 1.6879 + // element of node's data has general category M when interpreted 1.6880 + // as a Unicode code point, add one to end offset." 1.6881 + // 1.6882 + // TODO: Not even going to try handling anything beyond the most 1.6883 + // basic combining marks, since I couldn't find a good list. I 1.6884 + // special-case a few Hebrew diacritics too to test basic coverage 1.6885 + // of non-Latin stuff. 1.6886 + while (endOffset != node.length 1.6887 + && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) { 1.6888 + endOffset++; 1.6889 + } 1.6890 + 1.6891 + // "Call collapse(node, offset) on the context object's Selection." 1.6892 + getSelection().collapse(node, offset); 1.6893 + getActiveRange().setStart(node, offset); 1.6894 + 1.6895 + // "Call extend(node, end offset) on the context object's 1.6896 + // Selection." 1.6897 + getSelection().extend(node, endOffset); 1.6898 + getActiveRange().setEnd(node, endOffset); 1.6899 + 1.6900 + // "Delete the selection." 1.6901 + deleteSelection(); 1.6902 + 1.6903 + // "Return true." 1.6904 + return true; 1.6905 + } 1.6906 + 1.6907 + // "If node is an inline node, return true." 1.6908 + if (isInlineNode(node)) { 1.6909 + return true; 1.6910 + } 1.6911 + 1.6912 + // "If node has a child with index offset and that child is a br or hr 1.6913 + // or img, but is not a collapsed block prop:" 1.6914 + if (offset < node.childNodes.length 1.6915 + && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"]) 1.6916 + && !isCollapsedBlockProp(node.childNodes[offset])) { 1.6917 + // "Call collapse(node, offset) on the context object's Selection." 1.6918 + getSelection().collapse(node, offset); 1.6919 + getActiveRange().setStart(node, offset); 1.6920 + 1.6921 + // "Call extend(node, offset + 1) on the context object's 1.6922 + // Selection." 1.6923 + getSelection().extend(node, offset + 1); 1.6924 + getActiveRange().setEnd(node, offset + 1); 1.6925 + 1.6926 + // "Delete the selection." 1.6927 + deleteSelection(); 1.6928 + 1.6929 + // "Return true." 1.6930 + return true; 1.6931 + } 1.6932 + 1.6933 + // "Let end node equal node and let end offset equal offset." 1.6934 + var endNode = node; 1.6935 + var endOffset = offset; 1.6936 + 1.6937 + // "If end node has a child with index end offset, and that child is a 1.6938 + // collapsed block prop, add one to end offset." 1.6939 + if (endOffset < endNode.childNodes.length 1.6940 + && isCollapsedBlockProp(endNode.childNodes[endOffset])) { 1.6941 + endOffset++; 1.6942 + } 1.6943 + 1.6944 + // "Repeat the following steps:" 1.6945 + while (true) { 1.6946 + // "If end offset is the length of end node, set end offset to one 1.6947 + // plus the index of end node and then set end node to its parent." 1.6948 + if (endOffset == getNodeLength(endNode)) { 1.6949 + endOffset = 1 + getNodeIndex(endNode); 1.6950 + endNode = endNode.parentNode; 1.6951 + 1.6952 + // "Otherwise, if end node has a an editable invisible child with 1.6953 + // index end offset, remove it from end node." 1.6954 + } else if (endOffset < endNode.childNodes.length 1.6955 + && isEditable(endNode.childNodes[endOffset]) 1.6956 + && isInvisible(endNode.childNodes[endOffset])) { 1.6957 + endNode.removeChild(endNode.childNodes[endOffset]); 1.6958 + 1.6959 + // "Otherwise, break from this loop." 1.6960 + } else { 1.6961 + break; 1.6962 + } 1.6963 + } 1.6964 + 1.6965 + // "If the child of end node with index end offset minus one is a 1.6966 + // table, return true." 1.6967 + if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) { 1.6968 + return true; 1.6969 + } 1.6970 + 1.6971 + // "If the child of end node with index end offset is a table:" 1.6972 + if (isHtmlElement(endNode.childNodes[endOffset], "table")) { 1.6973 + // "Call collapse(end node, end offset) on the context object's 1.6974 + // Selection." 1.6975 + getSelection().collapse(endNode, endOffset); 1.6976 + getActiveRange().setStart(endNode, endOffset); 1.6977 + 1.6978 + // "Call extend(end node, end offset + 1) on the context object's 1.6979 + // Selection." 1.6980 + getSelection().extend(endNode, endOffset + 1); 1.6981 + getActiveRange().setEnd(endNode, endOffset + 1); 1.6982 + 1.6983 + // "Return true." 1.6984 + return true; 1.6985 + } 1.6986 + 1.6987 + // "If offset is the length of node, and the child of end node with 1.6988 + // index end offset is an hr or br:" 1.6989 + if (offset == getNodeLength(node) 1.6990 + && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) { 1.6991 + // "Call collapse(end node, end offset) on the context object's 1.6992 + // Selection." 1.6993 + getSelection().collapse(endNode, endOffset); 1.6994 + getActiveRange().setStart(endNode, endOffset); 1.6995 + 1.6996 + // "Call extend(end node, end offset + 1) on the context object's 1.6997 + // Selection." 1.6998 + getSelection().extend(endNode, endOffset + 1); 1.6999 + getActiveRange().setEnd(endNode, endOffset + 1); 1.7000 + 1.7001 + // "Delete the selection." 1.7002 + deleteSelection(); 1.7003 + 1.7004 + // "Call collapse(node, offset) on the Selection." 1.7005 + getSelection().collapse(node, offset); 1.7006 + getActiveRange().setStart(node, offset); 1.7007 + getActiveRange().collapse(true); 1.7008 + 1.7009 + // "Return true." 1.7010 + return true; 1.7011 + } 1.7012 + 1.7013 + // "While end node has a child with index end offset:" 1.7014 + while (endOffset < endNode.childNodes.length) { 1.7015 + // "If end node's child with index end offset is editable and 1.7016 + // invisible, remove it from end node." 1.7017 + if (isEditable(endNode.childNodes[endOffset]) 1.7018 + && isInvisible(endNode.childNodes[endOffset])) { 1.7019 + endNode.removeChild(endNode.childNodes[endOffset]); 1.7020 + 1.7021 + // "Otherwise, set end node to its child with index end offset and 1.7022 + // set end offset to zero." 1.7023 + } else { 1.7024 + endNode = endNode.childNodes[endOffset]; 1.7025 + endOffset = 0; 1.7026 + } 1.7027 + } 1.7028 + 1.7029 + // "Call collapse(node, offset) on the context object's Selection." 1.7030 + getSelection().collapse(node, offset); 1.7031 + getActiveRange().setStart(node, offset); 1.7032 + 1.7033 + // "Call extend(end node, end offset) on the context object's 1.7034 + // Selection." 1.7035 + getSelection().extend(endNode, endOffset); 1.7036 + getActiveRange().setEnd(endNode, endOffset); 1.7037 + 1.7038 + // "Delete the selection." 1.7039 + deleteSelection(); 1.7040 + 1.7041 + // "Return true." 1.7042 + return true; 1.7043 + } 1.7044 +}; 1.7045 + 1.7046 +//@} 1.7047 +///// The indent command ///// 1.7048 +//@{ 1.7049 +commands.indent = { 1.7050 + preservesOverrides: true, 1.7051 + action: function() { 1.7052 + // "Let items be a list of all lis that are ancestor containers of the 1.7053 + // active range's start and/or end node." 1.7054 + // 1.7055 + // Has to be in tree order, remember! 1.7056 + var items = []; 1.7057 + for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 1.7058 + if (isHtmlElement(node, "LI")) { 1.7059 + items.unshift(node); 1.7060 + } 1.7061 + } 1.7062 + for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 1.7063 + if (isHtmlElement(node, "LI")) { 1.7064 + items.unshift(node); 1.7065 + } 1.7066 + } 1.7067 + for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) { 1.7068 + if (isHtmlElement(node, "LI")) { 1.7069 + items.unshift(node); 1.7070 + } 1.7071 + } 1.7072 + 1.7073 + // "For each item in items, normalize sublists of item." 1.7074 + for (var i = 0; i < items.length; i++) { 1.7075 + normalizeSublists(items[i]); 1.7076 + } 1.7077 + 1.7078 + // "Block-extend the active range, and let new range be the result." 1.7079 + var newRange = blockExtend(getActiveRange()); 1.7080 + 1.7081 + // "Let node list be a list of nodes, initially empty." 1.7082 + var nodeList = []; 1.7083 + 1.7084 + // "For each node node contained in new range, if node is editable and 1.7085 + // is an allowed child of "div" or "ol" and if the last member of node 1.7086 + // list (if any) is not an ancestor of node, append node to node list." 1.7087 + nodeList = getContainedNodes(newRange, function(node) { 1.7088 + return isEditable(node) 1.7089 + && (isAllowedChild(node, "div") 1.7090 + || isAllowedChild(node, "ol")); 1.7091 + }); 1.7092 + 1.7093 + // "If the first visible member of node list is an li whose parent is 1.7094 + // an ol or ul:" 1.7095 + if (isHtmlElement(nodeList.filter(isVisible)[0], "li") 1.7096 + && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) { 1.7097 + // "Let sibling be node list's first visible member's 1.7098 + // previousSibling." 1.7099 + var sibling = nodeList.filter(isVisible)[0].previousSibling; 1.7100 + 1.7101 + // "While sibling is invisible, set sibling to its 1.7102 + // previousSibling." 1.7103 + while (isInvisible(sibling)) { 1.7104 + sibling = sibling.previousSibling; 1.7105 + } 1.7106 + 1.7107 + // "If sibling is an li, normalize sublists of sibling." 1.7108 + if (isHtmlElement(sibling, "li")) { 1.7109 + normalizeSublists(sibling); 1.7110 + } 1.7111 + } 1.7112 + 1.7113 + // "While node list is not empty:" 1.7114 + while (nodeList.length) { 1.7115 + // "Let sublist be a list of nodes, initially empty." 1.7116 + var sublist = []; 1.7117 + 1.7118 + // "Remove the first member of node list and append it to sublist." 1.7119 + sublist.push(nodeList.shift()); 1.7120 + 1.7121 + // "While the first member of node list is the nextSibling of the 1.7122 + // last member of sublist, remove the first member of node list and 1.7123 + // append it to sublist." 1.7124 + while (nodeList.length 1.7125 + && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 1.7126 + sublist.push(nodeList.shift()); 1.7127 + } 1.7128 + 1.7129 + // "Indent sublist." 1.7130 + indentNodes(sublist); 1.7131 + } 1.7132 + 1.7133 + // "Return true." 1.7134 + return true; 1.7135 + } 1.7136 +}; 1.7137 + 1.7138 +//@} 1.7139 +///// The insertHorizontalRule command ///// 1.7140 +//@{ 1.7141 +commands.inserthorizontalrule = { 1.7142 + preservesOverrides: true, 1.7143 + action: function() { 1.7144 + // "Let start node, start offset, end node, and end offset be the 1.7145 + // active range's start and end nodes and offsets." 1.7146 + var startNode = getActiveRange().startContainer; 1.7147 + var startOffset = getActiveRange().startOffset; 1.7148 + var endNode = getActiveRange().endContainer; 1.7149 + var endOffset = getActiveRange().endOffset; 1.7150 + 1.7151 + // "While start offset is 0 and start node's parent is not null, set 1.7152 + // start offset to start node's index, then set start node to its 1.7153 + // parent." 1.7154 + while (startOffset == 0 1.7155 + && startNode.parentNode) { 1.7156 + startOffset = getNodeIndex(startNode); 1.7157 + startNode = startNode.parentNode; 1.7158 + } 1.7159 + 1.7160 + // "While end offset is end node's length, and end node's parent is not 1.7161 + // null, set end offset to one plus end node's index, then set end node 1.7162 + // to its parent." 1.7163 + while (endOffset == getNodeLength(endNode) 1.7164 + && endNode.parentNode) { 1.7165 + endOffset = 1 + getNodeIndex(endNode); 1.7166 + endNode = endNode.parentNode; 1.7167 + } 1.7168 + 1.7169 + // "Call collapse(start node, start offset) on the context object's 1.7170 + // Selection." 1.7171 + getSelection().collapse(startNode, startOffset); 1.7172 + getActiveRange().setStart(startNode, startOffset); 1.7173 + 1.7174 + // "Call extend(end node, end offset) on the context object's 1.7175 + // Selection." 1.7176 + getSelection().extend(endNode, endOffset); 1.7177 + getActiveRange().setEnd(endNode, endOffset); 1.7178 + 1.7179 + // "Delete the selection, with block merging false." 1.7180 + deleteSelection({blockMerging: false}); 1.7181 + 1.7182 + // "If the active range's start node is neither editable nor an editing 1.7183 + // host, return true." 1.7184 + if (!isEditable(getActiveRange().startContainer) 1.7185 + && !isEditingHost(getActiveRange().startContainer)) { 1.7186 + return true; 1.7187 + } 1.7188 + 1.7189 + // "If the active range's start node is a Text node and its start 1.7190 + // offset is zero, call collapse() on the context object's Selection, 1.7191 + // with first argument the active range's start node's parent and 1.7192 + // second argument the active range's start node's index." 1.7193 + if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.7194 + && getActiveRange().startOffset == 0) { 1.7195 + var newNode = getActiveRange().startContainer.parentNode; 1.7196 + var newOffset = getNodeIndex(getActiveRange().startContainer); 1.7197 + getSelection().collapse(newNode, newOffset); 1.7198 + getActiveRange().setStart(newNode, newOffset); 1.7199 + getActiveRange().collapse(true); 1.7200 + } 1.7201 + 1.7202 + // "If the active range's start node is a Text node and its start 1.7203 + // offset is the length of its start node, call collapse() on the 1.7204 + // context object's Selection, with first argument the active range's 1.7205 + // start node's parent, and the second argument one plus the active 1.7206 + // range's start node's index." 1.7207 + if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.7208 + && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { 1.7209 + var newNode = getActiveRange().startContainer.parentNode; 1.7210 + var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); 1.7211 + getSelection().collapse(newNode, newOffset); 1.7212 + getActiveRange().setStart(newNode, newOffset); 1.7213 + getActiveRange().collapse(true); 1.7214 + } 1.7215 + 1.7216 + // "Let hr be the result of calling createElement("hr") on the 1.7217 + // context object." 1.7218 + var hr = document.createElement("hr"); 1.7219 + 1.7220 + // "Run insertNode(hr) on the active range." 1.7221 + getActiveRange().insertNode(hr); 1.7222 + 1.7223 + // "Fix disallowed ancestors of hr." 1.7224 + fixDisallowedAncestors(hr); 1.7225 + 1.7226 + // "Run collapse() on the context object's Selection, with first 1.7227 + // argument hr's parent and the second argument equal to one plus hr's 1.7228 + // index." 1.7229 + getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr)); 1.7230 + getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr)); 1.7231 + getActiveRange().collapse(true); 1.7232 + 1.7233 + // "Return true." 1.7234 + return true; 1.7235 + } 1.7236 +}; 1.7237 + 1.7238 +//@} 1.7239 +///// The insertHTML command ///// 1.7240 +//@{ 1.7241 +commands.inserthtml = { 1.7242 + preservesOverrides: true, 1.7243 + action: function(value) { 1.7244 + // "Delete the selection." 1.7245 + deleteSelection(); 1.7246 + 1.7247 + // "If the active range's start node is neither editable nor an editing 1.7248 + // host, return true." 1.7249 + if (!isEditable(getActiveRange().startContainer) 1.7250 + && !isEditingHost(getActiveRange().startContainer)) { 1.7251 + return true; 1.7252 + } 1.7253 + 1.7254 + // "Let frag be the result of calling createContextualFragment(value) 1.7255 + // on the active range." 1.7256 + var frag = getActiveRange().createContextualFragment(value); 1.7257 + 1.7258 + // "Let last child be the lastChild of frag." 1.7259 + var lastChild = frag.lastChild; 1.7260 + 1.7261 + // "If last child is null, return true." 1.7262 + if (!lastChild) { 1.7263 + return true; 1.7264 + } 1.7265 + 1.7266 + // "Let descendants be all descendants of frag." 1.7267 + var descendants = getDescendants(frag); 1.7268 + 1.7269 + // "If the active range's start node is a block node:" 1.7270 + if (isBlockNode(getActiveRange().startContainer)) { 1.7271 + // "Let collapsed block props be all editable collapsed block prop 1.7272 + // children of the active range's start node that have index 1.7273 + // greater than or equal to the active range's start offset." 1.7274 + // 1.7275 + // "For each node in collapsed block props, remove node from its 1.7276 + // parent." 1.7277 + [].filter.call(getActiveRange().startContainer.childNodes, function(node) { 1.7278 + return isEditable(node) 1.7279 + && isCollapsedBlockProp(node) 1.7280 + && getNodeIndex(node) >= getActiveRange().startOffset; 1.7281 + }).forEach(function(node) { 1.7282 + node.parentNode.removeChild(node); 1.7283 + }); 1.7284 + } 1.7285 + 1.7286 + // "Call insertNode(frag) on the active range." 1.7287 + getActiveRange().insertNode(frag); 1.7288 + 1.7289 + // "If the active range's start node is a block node with no visible 1.7290 + // children, call createElement("br") on the context object and append 1.7291 + // the result as the last child of the active range's start node." 1.7292 + if (isBlockNode(getActiveRange().startContainer) 1.7293 + && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) { 1.7294 + getActiveRange().startContainer.appendChild(document.createElement("br")); 1.7295 + } 1.7296 + 1.7297 + // "Call collapse() on the context object's Selection, with last 1.7298 + // child's parent as the first argument and one plus its index as the 1.7299 + // second." 1.7300 + getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild)); 1.7301 + getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild)); 1.7302 + 1.7303 + // "Fix disallowed ancestors of each member of descendants." 1.7304 + for (var i = 0; i < descendants.length; i++) { 1.7305 + fixDisallowedAncestors(descendants[i]); 1.7306 + } 1.7307 + 1.7308 + // "Return true." 1.7309 + return true; 1.7310 + } 1.7311 +}; 1.7312 + 1.7313 +//@} 1.7314 +///// The insertImage command ///// 1.7315 +//@{ 1.7316 +commands.insertimage = { 1.7317 + preservesOverrides: true, 1.7318 + action: function(value) { 1.7319 + // "If value is the empty string, return false." 1.7320 + if (value === "") { 1.7321 + return false; 1.7322 + } 1.7323 + 1.7324 + // "Delete the selection, with strip wrappers false." 1.7325 + deleteSelection({stripWrappers: false}); 1.7326 + 1.7327 + // "Let range be the active range." 1.7328 + var range = getActiveRange(); 1.7329 + 1.7330 + // "If the active range's start node is neither editable nor an editing 1.7331 + // host, return true." 1.7332 + if (!isEditable(getActiveRange().startContainer) 1.7333 + && !isEditingHost(getActiveRange().startContainer)) { 1.7334 + return true; 1.7335 + } 1.7336 + 1.7337 + // "If range's start node is a block node whose sole child is a br, and 1.7338 + // its start offset is 0, remove its start node's child from it." 1.7339 + if (isBlockNode(range.startContainer) 1.7340 + && range.startContainer.childNodes.length == 1 1.7341 + && isHtmlElement(range.startContainer.firstChild, "br") 1.7342 + && range.startOffset == 0) { 1.7343 + range.startContainer.removeChild(range.startContainer.firstChild); 1.7344 + } 1.7345 + 1.7346 + // "Let img be the result of calling createElement("img") on the 1.7347 + // context object." 1.7348 + var img = document.createElement("img"); 1.7349 + 1.7350 + // "Run setAttribute("src", value) on img." 1.7351 + img.setAttribute("src", value); 1.7352 + 1.7353 + // "Run insertNode(img) on the range." 1.7354 + range.insertNode(img); 1.7355 + 1.7356 + // "Run collapse() on the Selection, with first argument equal to the 1.7357 + // parent of img and the second argument equal to one plus the index of 1.7358 + // img." 1.7359 + // 1.7360 + // Not everyone actually supports collapse(), so we do it manually 1.7361 + // instead. Also, we need to modify the actual range we're given as 1.7362 + // well, for the sake of autoimplementation.html's range-filling-in. 1.7363 + range.setStart(img.parentNode, 1 + getNodeIndex(img)); 1.7364 + range.setEnd(img.parentNode, 1 + getNodeIndex(img)); 1.7365 + getSelection().removeAllRanges(); 1.7366 + getSelection().addRange(range); 1.7367 + 1.7368 + // IE adds width and height attributes for some reason, so remove those 1.7369 + // to actually do what the spec says. 1.7370 + img.removeAttribute("width"); 1.7371 + img.removeAttribute("height"); 1.7372 + 1.7373 + // "Return true." 1.7374 + return true; 1.7375 + } 1.7376 +}; 1.7377 + 1.7378 +//@} 1.7379 +///// The insertLineBreak command ///// 1.7380 +//@{ 1.7381 +commands.insertlinebreak = { 1.7382 + preservesOverrides: true, 1.7383 + action: function(value) { 1.7384 + // "Delete the selection, with strip wrappers false." 1.7385 + deleteSelection({stripWrappers: false}); 1.7386 + 1.7387 + // "If the active range's start node is neither editable nor an editing 1.7388 + // host, return true." 1.7389 + if (!isEditable(getActiveRange().startContainer) 1.7390 + && !isEditingHost(getActiveRange().startContainer)) { 1.7391 + return true; 1.7392 + } 1.7393 + 1.7394 + // "If the active range's start node is an Element, and "br" is not an 1.7395 + // allowed child of it, return true." 1.7396 + if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE 1.7397 + && !isAllowedChild("br", getActiveRange().startContainer)) { 1.7398 + return true; 1.7399 + } 1.7400 + 1.7401 + // "If the active range's start node is not an Element, and "br" is not 1.7402 + // an allowed child of the active range's start node's parent, return 1.7403 + // true." 1.7404 + if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE 1.7405 + && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) { 1.7406 + return true; 1.7407 + } 1.7408 + 1.7409 + // "If the active range's start node is a Text node and its start 1.7410 + // offset is zero, call collapse() on the context object's Selection, 1.7411 + // with first argument equal to the active range's start node's parent 1.7412 + // and second argument equal to the active range's start node's index." 1.7413 + if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.7414 + && getActiveRange().startOffset == 0) { 1.7415 + var newNode = getActiveRange().startContainer.parentNode; 1.7416 + var newOffset = getNodeIndex(getActiveRange().startContainer); 1.7417 + getSelection().collapse(newNode, newOffset); 1.7418 + getActiveRange().setStart(newNode, newOffset); 1.7419 + getActiveRange().setEnd(newNode, newOffset); 1.7420 + } 1.7421 + 1.7422 + // "If the active range's start node is a Text node and its start 1.7423 + // offset is the length of its start node, call collapse() on the 1.7424 + // context object's Selection, with first argument equal to the active 1.7425 + // range's start node's parent and second argument equal to one plus 1.7426 + // the active range's start node's index." 1.7427 + if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE 1.7428 + && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { 1.7429 + var newNode = getActiveRange().startContainer.parentNode; 1.7430 + var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); 1.7431 + getSelection().collapse(newNode, newOffset); 1.7432 + getActiveRange().setStart(newNode, newOffset); 1.7433 + getActiveRange().setEnd(newNode, newOffset); 1.7434 + } 1.7435 + 1.7436 + // "Let br be the result of calling createElement("br") on the context 1.7437 + // object." 1.7438 + var br = document.createElement("br"); 1.7439 + 1.7440 + // "Call insertNode(br) on the active range." 1.7441 + getActiveRange().insertNode(br); 1.7442 + 1.7443 + // "Call collapse() on the context object's Selection, with br's parent 1.7444 + // as the first argument and one plus br's index as the second 1.7445 + // argument." 1.7446 + getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); 1.7447 + getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); 1.7448 + getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); 1.7449 + 1.7450 + // "If br is a collapsed line break, call createElement("br") on the 1.7451 + // context object and let extra br be the result, then call 1.7452 + // insertNode(extra br) on the active range." 1.7453 + if (isCollapsedLineBreak(br)) { 1.7454 + getActiveRange().insertNode(document.createElement("br")); 1.7455 + 1.7456 + // Compensate for nonstandard implementations of insertNode 1.7457 + getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); 1.7458 + getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); 1.7459 + getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); 1.7460 + } 1.7461 + 1.7462 + // "Return true." 1.7463 + return true; 1.7464 + } 1.7465 +}; 1.7466 + 1.7467 +//@} 1.7468 +///// The insertOrderedList command ///// 1.7469 +//@{ 1.7470 +commands.insertorderedlist = { 1.7471 + preservesOverrides: true, 1.7472 + // "Toggle lists with tag name "ol", then return true." 1.7473 + action: function() { toggleLists("ol"); return true }, 1.7474 + // "True if the selection's list state is "mixed" or "mixed ol", false 1.7475 + // otherwise." 1.7476 + indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) }, 1.7477 + // "True if the selection's list state is "ol", false otherwise." 1.7478 + state: function() { return getSelectionListState() == "ol" }, 1.7479 +}; 1.7480 + 1.7481 +//@} 1.7482 +///// The insertParagraph command ///// 1.7483 +//@{ 1.7484 +commands.insertparagraph = { 1.7485 + preservesOverrides: true, 1.7486 + action: function() { 1.7487 + // "Delete the selection." 1.7488 + deleteSelection(); 1.7489 + 1.7490 + // "If the active range's start node is neither editable nor an editing 1.7491 + // host, return true." 1.7492 + if (!isEditable(getActiveRange().startContainer) 1.7493 + && !isEditingHost(getActiveRange().startContainer)) { 1.7494 + return true; 1.7495 + } 1.7496 + 1.7497 + // "Let node and offset be the active range's start node and offset." 1.7498 + var node = getActiveRange().startContainer; 1.7499 + var offset = getActiveRange().startOffset; 1.7500 + 1.7501 + // "If node is a Text node, and offset is neither 0 nor the length of 1.7502 + // node, call splitText(offset) on node." 1.7503 + if (node.nodeType == Node.TEXT_NODE 1.7504 + && offset != 0 1.7505 + && offset != getNodeLength(node)) { 1.7506 + node.splitText(offset); 1.7507 + } 1.7508 + 1.7509 + // "If node is a Text node and offset is its length, set offset to one 1.7510 + // plus the index of node, then set node to its parent." 1.7511 + if (node.nodeType == Node.TEXT_NODE 1.7512 + && offset == getNodeLength(node)) { 1.7513 + offset = 1 + getNodeIndex(node); 1.7514 + node = node.parentNode; 1.7515 + } 1.7516 + 1.7517 + // "If node is a Text or Comment node, set offset to the index of node, 1.7518 + // then set node to its parent." 1.7519 + if (node.nodeType == Node.TEXT_NODE 1.7520 + || node.nodeType == Node.COMMENT_NODE) { 1.7521 + offset = getNodeIndex(node); 1.7522 + node = node.parentNode; 1.7523 + } 1.7524 + 1.7525 + // "Call collapse(node, offset) on the context object's Selection." 1.7526 + getSelection().collapse(node, offset); 1.7527 + getActiveRange().setStart(node, offset); 1.7528 + getActiveRange().setEnd(node, offset); 1.7529 + 1.7530 + // "Let container equal node." 1.7531 + var container = node; 1.7532 + 1.7533 + // "While container is not a single-line container, and container's 1.7534 + // parent is editable and in the same editing host as node, set 1.7535 + // container to its parent." 1.7536 + while (!isSingleLineContainer(container) 1.7537 + && isEditable(container.parentNode) 1.7538 + && inSameEditingHost(node, container.parentNode)) { 1.7539 + container = container.parentNode; 1.7540 + } 1.7541 + 1.7542 + // "If container is an editable single-line container in the same 1.7543 + // editing host as node, and its local name is "p" or "div":" 1.7544 + if (isEditable(container) 1.7545 + && isSingleLineContainer(container) 1.7546 + && inSameEditingHost(node, container.parentNode) 1.7547 + && (container.tagName == "P" || container.tagName == "DIV")) { 1.7548 + // "Let outer container equal container." 1.7549 + var outerContainer = container; 1.7550 + 1.7551 + // "While outer container is not a dd or dt or li, and outer 1.7552 + // container's parent is editable, set outer container to its 1.7553 + // parent." 1.7554 + while (!isHtmlElement(outerContainer, ["dd", "dt", "li"]) 1.7555 + && isEditable(outerContainer.parentNode)) { 1.7556 + outerContainer = outerContainer.parentNode; 1.7557 + } 1.7558 + 1.7559 + // "If outer container is a dd or dt or li, set container to outer 1.7560 + // container." 1.7561 + if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) { 1.7562 + container = outerContainer; 1.7563 + } 1.7564 + } 1.7565 + 1.7566 + // "If container is not editable or not in the same editing host as 1.7567 + // node or is not a single-line container:" 1.7568 + if (!isEditable(container) 1.7569 + || !inSameEditingHost(container, node) 1.7570 + || !isSingleLineContainer(container)) { 1.7571 + // "Let tag be the default single-line container name." 1.7572 + var tag = defaultSingleLineContainerName; 1.7573 + 1.7574 + // "Block-extend the active range, and let new range be the 1.7575 + // result." 1.7576 + var newRange = blockExtend(getActiveRange()); 1.7577 + 1.7578 + // "Let node list be a list of nodes, initially empty." 1.7579 + // 1.7580 + // "Append to node list the first node in tree order that is 1.7581 + // contained in new range and is an allowed child of "p", if any." 1.7582 + var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") }) 1.7583 + .slice(0, 1); 1.7584 + 1.7585 + // "If node list is empty:" 1.7586 + if (!nodeList.length) { 1.7587 + // "If tag is not an allowed child of the active range's start 1.7588 + // node, return true." 1.7589 + if (!isAllowedChild(tag, getActiveRange().startContainer)) { 1.7590 + return true; 1.7591 + } 1.7592 + 1.7593 + // "Set container to the result of calling createElement(tag) 1.7594 + // on the context object." 1.7595 + container = document.createElement(tag); 1.7596 + 1.7597 + // "Call insertNode(container) on the active range." 1.7598 + getActiveRange().insertNode(container); 1.7599 + 1.7600 + // "Call createElement("br") on the context object, and append 1.7601 + // the result as the last child of container." 1.7602 + container.appendChild(document.createElement("br")); 1.7603 + 1.7604 + // "Call collapse(container, 0) on the context object's 1.7605 + // Selection." 1.7606 + getSelection().collapse(container, 0); 1.7607 + getActiveRange().setStart(container, 0); 1.7608 + getActiveRange().setEnd(container, 0); 1.7609 + 1.7610 + // "Return true." 1.7611 + return true; 1.7612 + } 1.7613 + 1.7614 + // "While the nextSibling of the last member of node list is not 1.7615 + // null and is an allowed child of "p", append it to node list." 1.7616 + while (nodeList[nodeList.length - 1].nextSibling 1.7617 + && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { 1.7618 + nodeList.push(nodeList[nodeList.length - 1].nextSibling); 1.7619 + } 1.7620 + 1.7621 + // "Wrap node list, with sibling criteria returning false and new 1.7622 + // parent instructions returning the result of calling 1.7623 + // createElement(tag) on the context object. Set container to the 1.7624 + // result." 1.7625 + container = wrap(nodeList, 1.7626 + function() { return false }, 1.7627 + function() { return document.createElement(tag) } 1.7628 + ); 1.7629 + } 1.7630 + 1.7631 + // "If container's local name is "address", "listing", or "pre":" 1.7632 + if (container.tagName == "ADDRESS" 1.7633 + || container.tagName == "LISTING" 1.7634 + || container.tagName == "PRE") { 1.7635 + // "Let br be the result of calling createElement("br") on the 1.7636 + // context object." 1.7637 + var br = document.createElement("br"); 1.7638 + 1.7639 + // "Call insertNode(br) on the active range." 1.7640 + getActiveRange().insertNode(br); 1.7641 + 1.7642 + // "Call collapse(node, offset + 1) on the context object's 1.7643 + // Selection." 1.7644 + getSelection().collapse(node, offset + 1); 1.7645 + getActiveRange().setStart(node, offset + 1); 1.7646 + getActiveRange().setEnd(node, offset + 1); 1.7647 + 1.7648 + // "If br is the last descendant of container, let br be the result 1.7649 + // of calling createElement("br") on the context object, then call 1.7650 + // insertNode(br) on the active range." 1.7651 + // 1.7652 + // Work around browser bugs: some browsers select the 1.7653 + // newly-inserted node, not per spec. 1.7654 + if (!isDescendant(nextNode(br), container)) { 1.7655 + getActiveRange().insertNode(document.createElement("br")); 1.7656 + getSelection().collapse(node, offset + 1); 1.7657 + getActiveRange().setEnd(node, offset + 1); 1.7658 + } 1.7659 + 1.7660 + // "Return true." 1.7661 + return true; 1.7662 + } 1.7663 + 1.7664 + // "If container's local name is "li", "dt", or "dd"; and either it has 1.7665 + // no children or it has a single child and that child is a br:" 1.7666 + if (["LI", "DT", "DD"].indexOf(container.tagName) != -1 1.7667 + && (!container.hasChildNodes() 1.7668 + || (container.childNodes.length == 1 1.7669 + && isHtmlElement(container.firstChild, "br")))) { 1.7670 + // "Split the parent of the one-node list consisting of container." 1.7671 + splitParent([container]); 1.7672 + 1.7673 + // "If container has no children, call createElement("br") on the 1.7674 + // context object and append the result as the last child of 1.7675 + // container." 1.7676 + if (!container.hasChildNodes()) { 1.7677 + container.appendChild(document.createElement("br")); 1.7678 + } 1.7679 + 1.7680 + // "If container is a dd or dt, and it is not an allowed child of 1.7681 + // any of its ancestors in the same editing host, set the tag name 1.7682 + // of container to the default single-line container name and let 1.7683 + // container be the result." 1.7684 + if (isHtmlElement(container, ["dd", "dt"]) 1.7685 + && getAncestors(container).every(function(ancestor) { 1.7686 + return !inSameEditingHost(container, ancestor) 1.7687 + || !isAllowedChild(container, ancestor) 1.7688 + })) { 1.7689 + container = setTagName(container, defaultSingleLineContainerName); 1.7690 + } 1.7691 + 1.7692 + // "Fix disallowed ancestors of container." 1.7693 + fixDisallowedAncestors(container); 1.7694 + 1.7695 + // "Return true." 1.7696 + return true; 1.7697 + } 1.7698 + 1.7699 + // "Let new line range be a new range whose start is the same as 1.7700 + // the active range's, and whose end is (container, length of 1.7701 + // container)." 1.7702 + var newLineRange = document.createRange(); 1.7703 + newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset); 1.7704 + newLineRange.setEnd(container, getNodeLength(container)); 1.7705 + 1.7706 + // "While new line range's start offset is zero and its start node is 1.7707 + // not a prohibited paragraph child, set its start to (parent of start 1.7708 + // node, index of start node)." 1.7709 + while (newLineRange.startOffset == 0 1.7710 + && !isProhibitedParagraphChild(newLineRange.startContainer)) { 1.7711 + newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer)); 1.7712 + } 1.7713 + 1.7714 + // "While new line range's start offset is the length of its start node 1.7715 + // and its start node is not a prohibited paragraph child, set its 1.7716 + // start to (parent of start node, 1 + index of start node)." 1.7717 + while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer) 1.7718 + && !isProhibitedParagraphChild(newLineRange.startContainer)) { 1.7719 + newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer)); 1.7720 + } 1.7721 + 1.7722 + // "Let end of line be true if new line range contains either nothing 1.7723 + // or a single br, and false otherwise." 1.7724 + var containedInNewLineRange = getContainedNodes(newLineRange); 1.7725 + var endOfLine = !containedInNewLineRange.length 1.7726 + || (containedInNewLineRange.length == 1 1.7727 + && isHtmlElement(containedInNewLineRange[0], "br")); 1.7728 + 1.7729 + // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or 1.7730 + // "h6", and end of line is true, let new container name be the default 1.7731 + // single-line container name." 1.7732 + var newContainerName; 1.7733 + if (/^H[1-6]$/.test(container.tagName) 1.7734 + && endOfLine) { 1.7735 + newContainerName = defaultSingleLineContainerName; 1.7736 + 1.7737 + // "Otherwise, if the local name of container is "dt" and end of line 1.7738 + // is true, let new container name be "dd"." 1.7739 + } else if (container.tagName == "DT" 1.7740 + && endOfLine) { 1.7741 + newContainerName = "dd"; 1.7742 + 1.7743 + // "Otherwise, if the local name of container is "dd" and end of line 1.7744 + // is true, let new container name be "dt"." 1.7745 + } else if (container.tagName == "DD" 1.7746 + && endOfLine) { 1.7747 + newContainerName = "dt"; 1.7748 + 1.7749 + // "Otherwise, let new container name be the local name of container." 1.7750 + } else { 1.7751 + newContainerName = container.tagName.toLowerCase(); 1.7752 + } 1.7753 + 1.7754 + // "Let new container be the result of calling createElement(new 1.7755 + // container name) on the context object." 1.7756 + var newContainer = document.createElement(newContainerName); 1.7757 + 1.7758 + // "Copy all attributes of container to new container." 1.7759 + for (var i = 0; i < container.attributes.length; i++) { 1.7760 + newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value); 1.7761 + } 1.7762 + 1.7763 + // "If new container has an id attribute, unset it." 1.7764 + newContainer.removeAttribute("id"); 1.7765 + 1.7766 + // "Insert new container into the parent of container immediately after 1.7767 + // container." 1.7768 + container.parentNode.insertBefore(newContainer, container.nextSibling); 1.7769 + 1.7770 + // "Let contained nodes be all nodes contained in new line range." 1.7771 + var containedNodes = getAllContainedNodes(newLineRange); 1.7772 + 1.7773 + // "Let frag be the result of calling extractContents() on new line 1.7774 + // range." 1.7775 + var frag = newLineRange.extractContents(); 1.7776 + 1.7777 + // "Unset the id attribute (if any) of each Element descendant of frag 1.7778 + // that is not in contained nodes." 1.7779 + var descendants = getDescendants(frag); 1.7780 + for (var i = 0; i < descendants.length; i++) { 1.7781 + if (descendants[i].nodeType == Node.ELEMENT_NODE 1.7782 + && containedNodes.indexOf(descendants[i]) == -1) { 1.7783 + descendants[i].removeAttribute("id"); 1.7784 + } 1.7785 + } 1.7786 + 1.7787 + // "Call appendChild(frag) on new container." 1.7788 + newContainer.appendChild(frag); 1.7789 + 1.7790 + // "While container's lastChild is a prohibited paragraph child, set 1.7791 + // container to its lastChild." 1.7792 + while (isProhibitedParagraphChild(container.lastChild)) { 1.7793 + container = container.lastChild; 1.7794 + } 1.7795 + 1.7796 + // "While new container's lastChild is a prohibited paragraph child, 1.7797 + // set new container to its lastChild." 1.7798 + while (isProhibitedParagraphChild(newContainer.lastChild)) { 1.7799 + newContainer = newContainer.lastChild; 1.7800 + } 1.7801 + 1.7802 + // "If container has no visible children, call createElement("br") on 1.7803 + // the context object, and append the result as the last child of 1.7804 + // container." 1.7805 + if (![].some.call(container.childNodes, isVisible)) { 1.7806 + container.appendChild(document.createElement("br")); 1.7807 + } 1.7808 + 1.7809 + // "If new container has no visible children, call createElement("br") 1.7810 + // on the context object, and append the result as the last child of 1.7811 + // new container." 1.7812 + if (![].some.call(newContainer.childNodes, isVisible)) { 1.7813 + newContainer.appendChild(document.createElement("br")); 1.7814 + } 1.7815 + 1.7816 + // "Call collapse(new container, 0) on the context object's Selection." 1.7817 + getSelection().collapse(newContainer, 0); 1.7818 + getActiveRange().setStart(newContainer, 0); 1.7819 + getActiveRange().setEnd(newContainer, 0); 1.7820 + 1.7821 + // "Return true." 1.7822 + return true; 1.7823 + } 1.7824 +}; 1.7825 + 1.7826 +//@} 1.7827 +///// The insertText command ///// 1.7828 +//@{ 1.7829 +commands.inserttext = { 1.7830 + action: function(value) { 1.7831 + // "Delete the selection, with strip wrappers false." 1.7832 + deleteSelection({stripWrappers: false}); 1.7833 + 1.7834 + // "If the active range's start node is neither editable nor an editing 1.7835 + // host, return true." 1.7836 + if (!isEditable(getActiveRange().startContainer) 1.7837 + && !isEditingHost(getActiveRange().startContainer)) { 1.7838 + return true; 1.7839 + } 1.7840 + 1.7841 + // "If value's length is greater than one:" 1.7842 + if (value.length > 1) { 1.7843 + // "For each element el in value, take the action for the 1.7844 + // insertText command, with value equal to el." 1.7845 + for (var i = 0; i < value.length; i++) { 1.7846 + commands.inserttext.action(value[i]); 1.7847 + } 1.7848 + 1.7849 + // "Return true." 1.7850 + return true; 1.7851 + } 1.7852 + 1.7853 + // "If value is the empty string, return true." 1.7854 + if (value == "") { 1.7855 + return true; 1.7856 + } 1.7857 + 1.7858 + // "If value is a newline (U+00A0), take the action for the 1.7859 + // insertParagraph command and return true." 1.7860 + if (value == "\n") { 1.7861 + commands.insertparagraph.action(); 1.7862 + return true; 1.7863 + } 1.7864 + 1.7865 + // "Let node and offset be the active range's start node and offset." 1.7866 + var node = getActiveRange().startContainer; 1.7867 + var offset = getActiveRange().startOffset; 1.7868 + 1.7869 + // "If node has a child whose index is offset − 1, and that child is a 1.7870 + // Text node, set node to that child, then set offset to node's 1.7871 + // length." 1.7872 + if (0 <= offset - 1 1.7873 + && offset - 1 < node.childNodes.length 1.7874 + && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) { 1.7875 + node = node.childNodes[offset - 1]; 1.7876 + offset = getNodeLength(node); 1.7877 + } 1.7878 + 1.7879 + // "If node has a child whose index is offset, and that child is a Text 1.7880 + // node, set node to that child, then set offset to zero." 1.7881 + if (0 <= offset 1.7882 + && offset < node.childNodes.length 1.7883 + && node.childNodes[offset].nodeType == Node.TEXT_NODE) { 1.7884 + node = node.childNodes[offset]; 1.7885 + offset = 0; 1.7886 + } 1.7887 + 1.7888 + // "Record current overrides, and let overrides be the result." 1.7889 + var overrides = recordCurrentOverrides(); 1.7890 + 1.7891 + // "Call collapse(node, offset) on the context object's Selection." 1.7892 + getSelection().collapse(node, offset); 1.7893 + getActiveRange().setStart(node, offset); 1.7894 + getActiveRange().setEnd(node, offset); 1.7895 + 1.7896 + // "Canonicalize whitespace at (node, offset)." 1.7897 + canonicalizeWhitespace(node, offset); 1.7898 + 1.7899 + // "Let (node, offset) be the active range's start." 1.7900 + node = getActiveRange().startContainer; 1.7901 + offset = getActiveRange().startOffset; 1.7902 + 1.7903 + // "If node is a Text node:" 1.7904 + if (node.nodeType == Node.TEXT_NODE) { 1.7905 + // "Call insertData(offset, value) on node." 1.7906 + node.insertData(offset, value); 1.7907 + 1.7908 + // "Call collapse(node, offset) on the context object's Selection." 1.7909 + getSelection().collapse(node, offset); 1.7910 + getActiveRange().setStart(node, offset); 1.7911 + 1.7912 + // "Call extend(node, offset + 1) on the context object's 1.7913 + // Selection." 1.7914 + // 1.7915 + // Work around WebKit bug: the extend() can throw if the text we're 1.7916 + // adding is trailing whitespace. 1.7917 + try { getSelection().extend(node, offset + 1); } catch(e) {} 1.7918 + getActiveRange().setEnd(node, offset + 1); 1.7919 + 1.7920 + // "Otherwise:" 1.7921 + } else { 1.7922 + // "If node has only one child, which is a collapsed line break, 1.7923 + // remove its child from it." 1.7924 + // 1.7925 + // FIXME: IE incorrectly returns false here instead of true 1.7926 + // sometimes? 1.7927 + if (node.childNodes.length == 1 1.7928 + && isCollapsedLineBreak(node.firstChild)) { 1.7929 + node.removeChild(node.firstChild); 1.7930 + } 1.7931 + 1.7932 + // "Let text be the result of calling createTextNode(value) on the 1.7933 + // context object." 1.7934 + var text = document.createTextNode(value); 1.7935 + 1.7936 + // "Call insertNode(text) on the active range." 1.7937 + getActiveRange().insertNode(text); 1.7938 + 1.7939 + // "Call collapse(text, 0) on the context object's Selection." 1.7940 + getSelection().collapse(text, 0); 1.7941 + getActiveRange().setStart(text, 0); 1.7942 + 1.7943 + // "Call extend(text, 1) on the context object's Selection." 1.7944 + getSelection().extend(text, 1); 1.7945 + getActiveRange().setEnd(text, 1); 1.7946 + } 1.7947 + 1.7948 + // "Restore states and values from overrides." 1.7949 + restoreStatesAndValues(overrides); 1.7950 + 1.7951 + // "Canonicalize whitespace at the active range's start, with fix 1.7952 + // collapsed space false." 1.7953 + canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); 1.7954 + 1.7955 + // "Canonicalize whitespace at the active range's end, with fix 1.7956 + // collapsed space false." 1.7957 + canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); 1.7958 + 1.7959 + // "If value is a space character, autolink the active range's start." 1.7960 + if (/^[ \t\n\f\r]$/.test(value)) { 1.7961 + autolink(getActiveRange().startContainer, getActiveRange().startOffset); 1.7962 + } 1.7963 + 1.7964 + // "Call collapseToEnd() on the context object's Selection." 1.7965 + // 1.7966 + // Work around WebKit bug: sometimes it blows up the selection and 1.7967 + // throws, which we don't want. 1.7968 + try { getSelection().collapseToEnd(); } catch(e) {} 1.7969 + getActiveRange().collapse(false); 1.7970 + 1.7971 + // "Return true." 1.7972 + return true; 1.7973 + } 1.7974 +}; 1.7975 + 1.7976 +//@} 1.7977 +///// The insertUnorderedList command ///// 1.7978 +//@{ 1.7979 +commands.insertunorderedlist = { 1.7980 + preservesOverrides: true, 1.7981 + // "Toggle lists with tag name "ul", then return true." 1.7982 + action: function() { toggleLists("ul"); return true }, 1.7983 + // "True if the selection's list state is "mixed" or "mixed ul", false 1.7984 + // otherwise." 1.7985 + indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) }, 1.7986 + // "True if the selection's list state is "ul", false otherwise." 1.7987 + state: function() { return getSelectionListState() == "ul" }, 1.7988 +}; 1.7989 + 1.7990 +//@} 1.7991 +///// The justifyCenter command ///// 1.7992 +//@{ 1.7993 +commands.justifycenter = { 1.7994 + preservesOverrides: true, 1.7995 + // "Justify the selection with alignment "center", then return true." 1.7996 + action: function() { justifySelection("center"); return true }, 1.7997 + indeterm: function() { 1.7998 + // "Return false if the active range is null. Otherwise, block-extend 1.7999 + // the active range. Return true if among visible editable nodes that 1.8000 + // are contained in the result and have no children, at least one has 1.8001 + // alignment value "center" and at least one does not. Otherwise return 1.8002 + // false." 1.8003 + if (!getActiveRange()) { 1.8004 + return false; 1.8005 + } 1.8006 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8007 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8008 + }); 1.8009 + return nodes.some(function(node) { return getAlignmentValue(node) == "center" }) 1.8010 + && nodes.some(function(node) { return getAlignmentValue(node) != "center" }); 1.8011 + }, state: function() { 1.8012 + // "Return false if the active range is null. Otherwise, block-extend 1.8013 + // the active range. Return true if there is at least one visible 1.8014 + // editable node that is contained in the result and has no children, 1.8015 + // and all such nodes have alignment value "center". Otherwise return 1.8016 + // false." 1.8017 + if (!getActiveRange()) { 1.8018 + return false; 1.8019 + } 1.8020 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8021 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8022 + }); 1.8023 + return nodes.length 1.8024 + && nodes.every(function(node) { return getAlignmentValue(node) == "center" }); 1.8025 + }, value: function() { 1.8026 + // "Return the empty string if the active range is null. Otherwise, 1.8027 + // block-extend the active range, and return the alignment value of the 1.8028 + // first visible editable node that is contained in the result and has 1.8029 + // no children. If there is no such node, return "left"." 1.8030 + if (!getActiveRange()) { 1.8031 + return ""; 1.8032 + } 1.8033 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8034 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8035 + }); 1.8036 + if (nodes.length) { 1.8037 + return getAlignmentValue(nodes[0]); 1.8038 + } else { 1.8039 + return "left"; 1.8040 + } 1.8041 + }, 1.8042 +}; 1.8043 + 1.8044 +//@} 1.8045 +///// The justifyFull command ///// 1.8046 +//@{ 1.8047 +commands.justifyfull = { 1.8048 + preservesOverrides: true, 1.8049 + // "Justify the selection with alignment "justify", then return true." 1.8050 + action: function() { justifySelection("justify"); return true }, 1.8051 + indeterm: function() { 1.8052 + // "Return false if the active range is null. Otherwise, block-extend 1.8053 + // the active range. Return true if among visible editable nodes that 1.8054 + // are contained in the result and have no children, at least one has 1.8055 + // alignment value "justify" and at least one does not. Otherwise 1.8056 + // return false." 1.8057 + if (!getActiveRange()) { 1.8058 + return false; 1.8059 + } 1.8060 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8061 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8062 + }); 1.8063 + return nodes.some(function(node) { return getAlignmentValue(node) == "justify" }) 1.8064 + && nodes.some(function(node) { return getAlignmentValue(node) != "justify" }); 1.8065 + }, state: function() { 1.8066 + // "Return false if the active range is null. Otherwise, block-extend 1.8067 + // the active range. Return true if there is at least one visible 1.8068 + // editable node that is contained in the result and has no children, 1.8069 + // and all such nodes have alignment value "justify". Otherwise return 1.8070 + // false." 1.8071 + if (!getActiveRange()) { 1.8072 + return false; 1.8073 + } 1.8074 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8075 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8076 + }); 1.8077 + return nodes.length 1.8078 + && nodes.every(function(node) { return getAlignmentValue(node) == "justify" }); 1.8079 + }, value: function() { 1.8080 + // "Return the empty string if the active range is null. Otherwise, 1.8081 + // block-extend the active range, and return the alignment value of the 1.8082 + // first visible editable node that is contained in the result and has 1.8083 + // no children. If there is no such node, return "left"." 1.8084 + if (!getActiveRange()) { 1.8085 + return ""; 1.8086 + } 1.8087 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8088 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8089 + }); 1.8090 + if (nodes.length) { 1.8091 + return getAlignmentValue(nodes[0]); 1.8092 + } else { 1.8093 + return "left"; 1.8094 + } 1.8095 + }, 1.8096 +}; 1.8097 + 1.8098 +//@} 1.8099 +///// The justifyLeft command ///// 1.8100 +//@{ 1.8101 +commands.justifyleft = { 1.8102 + preservesOverrides: true, 1.8103 + // "Justify the selection with alignment "left", then return true." 1.8104 + action: function() { justifySelection("left"); return true }, 1.8105 + indeterm: function() { 1.8106 + // "Return false if the active range is null. Otherwise, block-extend 1.8107 + // the active range. Return true if among visible editable nodes that 1.8108 + // are contained in the result and have no children, at least one has 1.8109 + // alignment value "left" and at least one does not. Otherwise return 1.8110 + // false." 1.8111 + if (!getActiveRange()) { 1.8112 + return false; 1.8113 + } 1.8114 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8115 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8116 + }); 1.8117 + return nodes.some(function(node) { return getAlignmentValue(node) == "left" }) 1.8118 + && nodes.some(function(node) { return getAlignmentValue(node) != "left" }); 1.8119 + }, state: function() { 1.8120 + // "Return false if the active range is null. Otherwise, block-extend 1.8121 + // the active range. Return true if there is at least one visible 1.8122 + // editable node that is contained in the result and has no children, 1.8123 + // and all such nodes have alignment value "left". Otherwise return 1.8124 + // false." 1.8125 + if (!getActiveRange()) { 1.8126 + return false; 1.8127 + } 1.8128 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8129 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8130 + }); 1.8131 + return nodes.length 1.8132 + && nodes.every(function(node) { return getAlignmentValue(node) == "left" }); 1.8133 + }, value: function() { 1.8134 + // "Return the empty string if the active range is null. Otherwise, 1.8135 + // block-extend the active range, and return the alignment value of the 1.8136 + // first visible editable node that is contained in the result and has 1.8137 + // no children. If there is no such node, return "left"." 1.8138 + if (!getActiveRange()) { 1.8139 + return ""; 1.8140 + } 1.8141 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8142 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8143 + }); 1.8144 + if (nodes.length) { 1.8145 + return getAlignmentValue(nodes[0]); 1.8146 + } else { 1.8147 + return "left"; 1.8148 + } 1.8149 + }, 1.8150 +}; 1.8151 + 1.8152 +//@} 1.8153 +///// The justifyRight command ///// 1.8154 +//@{ 1.8155 +commands.justifyright = { 1.8156 + preservesOverrides: true, 1.8157 + // "Justify the selection with alignment "right", then return true." 1.8158 + action: function() { justifySelection("right"); return true }, 1.8159 + indeterm: function() { 1.8160 + // "Return false if the active range is null. Otherwise, block-extend 1.8161 + // the active range. Return true if among visible editable nodes that 1.8162 + // are contained in the result and have no children, at least one has 1.8163 + // alignment value "right" and at least one does not. Otherwise return 1.8164 + // false." 1.8165 + if (!getActiveRange()) { 1.8166 + return false; 1.8167 + } 1.8168 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8169 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8170 + }); 1.8171 + return nodes.some(function(node) { return getAlignmentValue(node) == "right" }) 1.8172 + && nodes.some(function(node) { return getAlignmentValue(node) != "right" }); 1.8173 + }, state: function() { 1.8174 + // "Return false if the active range is null. Otherwise, block-extend 1.8175 + // the active range. Return true if there is at least one visible 1.8176 + // editable node that is contained in the result and has no children, 1.8177 + // and all such nodes have alignment value "right". Otherwise return 1.8178 + // false." 1.8179 + if (!getActiveRange()) { 1.8180 + return false; 1.8181 + } 1.8182 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8183 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8184 + }); 1.8185 + return nodes.length 1.8186 + && nodes.every(function(node) { return getAlignmentValue(node) == "right" }); 1.8187 + }, value: function() { 1.8188 + // "Return the empty string if the active range is null. Otherwise, 1.8189 + // block-extend the active range, and return the alignment value of the 1.8190 + // first visible editable node that is contained in the result and has 1.8191 + // no children. If there is no such node, return "left"." 1.8192 + if (!getActiveRange()) { 1.8193 + return ""; 1.8194 + } 1.8195 + var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 1.8196 + return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 1.8197 + }); 1.8198 + if (nodes.length) { 1.8199 + return getAlignmentValue(nodes[0]); 1.8200 + } else { 1.8201 + return "left"; 1.8202 + } 1.8203 + }, 1.8204 +}; 1.8205 + 1.8206 +//@} 1.8207 +///// The outdent command ///// 1.8208 +//@{ 1.8209 +commands.outdent = { 1.8210 + preservesOverrides: true, 1.8211 + action: function() { 1.8212 + // "Let items be a list of all lis that are ancestor containers of the 1.8213 + // range's start and/or end node." 1.8214 + // 1.8215 + // It's annoying to get this in tree order using functional stuff 1.8216 + // without doing getDescendants(document), which is slow, so I do it 1.8217 + // imperatively. 1.8218 + var items = []; 1.8219 + (function(){ 1.8220 + for ( 1.8221 + var ancestorContainer = getActiveRange().endContainer; 1.8222 + ancestorContainer != getActiveRange().commonAncestorContainer; 1.8223 + ancestorContainer = ancestorContainer.parentNode 1.8224 + ) { 1.8225 + if (isHtmlElement(ancestorContainer, "li")) { 1.8226 + items.unshift(ancestorContainer); 1.8227 + } 1.8228 + } 1.8229 + for ( 1.8230 + var ancestorContainer = getActiveRange().startContainer; 1.8231 + ancestorContainer; 1.8232 + ancestorContainer = ancestorContainer.parentNode 1.8233 + ) { 1.8234 + if (isHtmlElement(ancestorContainer, "li")) { 1.8235 + items.unshift(ancestorContainer); 1.8236 + } 1.8237 + } 1.8238 + })(); 1.8239 + 1.8240 + // "For each item in items, normalize sublists of item." 1.8241 + items.forEach(normalizeSublists); 1.8242 + 1.8243 + // "Block-extend the active range, and let new range be the result." 1.8244 + var newRange = blockExtend(getActiveRange()); 1.8245 + 1.8246 + // "Let node list be a list of nodes, initially empty." 1.8247 + // 1.8248 + // "For each node node contained in new range, append node to node list 1.8249 + // if the last member of node list (if any) is not an ancestor of node; 1.8250 + // node is editable; and either node has no editable descendants, or is 1.8251 + // an ol or ul, or is an li whose parent is an ol or ul." 1.8252 + var nodeList = getContainedNodes(newRange, function(node) { 1.8253 + return isEditable(node) 1.8254 + && (!getDescendants(node).some(isEditable) 1.8255 + || isHtmlElement(node, ["ol", "ul"]) 1.8256 + || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"]))); 1.8257 + }); 1.8258 + 1.8259 + // "While node list is not empty:" 1.8260 + while (nodeList.length) { 1.8261 + // "While the first member of node list is an ol or ul or is not 1.8262 + // the child of an ol or ul, outdent it and remove it from node 1.8263 + // list." 1.8264 + while (nodeList.length 1.8265 + && (isHtmlElement(nodeList[0], ["OL", "UL"]) 1.8266 + || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) { 1.8267 + outdentNode(nodeList.shift()); 1.8268 + } 1.8269 + 1.8270 + // "If node list is empty, break from these substeps." 1.8271 + if (!nodeList.length) { 1.8272 + break; 1.8273 + } 1.8274 + 1.8275 + // "Let sublist be a list of nodes, initially empty." 1.8276 + var sublist = []; 1.8277 + 1.8278 + // "Remove the first member of node list and append it to sublist." 1.8279 + sublist.push(nodeList.shift()); 1.8280 + 1.8281 + // "While the first member of node list is the nextSibling of the 1.8282 + // last member of sublist, and the first member of node list is not 1.8283 + // an ol or ul, remove the first member of node list and append it 1.8284 + // to sublist." 1.8285 + while (nodeList.length 1.8286 + && nodeList[0] == sublist[sublist.length - 1].nextSibling 1.8287 + && !isHtmlElement(nodeList[0], ["OL", "UL"])) { 1.8288 + sublist.push(nodeList.shift()); 1.8289 + } 1.8290 + 1.8291 + // "Record the values of sublist, and let values be the result." 1.8292 + var values = recordValues(sublist); 1.8293 + 1.8294 + // "Split the parent of sublist, with new parent null." 1.8295 + splitParent(sublist); 1.8296 + 1.8297 + // "Fix disallowed ancestors of each member of sublist." 1.8298 + sublist.forEach(fixDisallowedAncestors); 1.8299 + 1.8300 + // "Restore the values from values." 1.8301 + restoreValues(values); 1.8302 + } 1.8303 + 1.8304 + // "Return true." 1.8305 + return true; 1.8306 + } 1.8307 +}; 1.8308 + 1.8309 +//@} 1.8310 + 1.8311 +////////////////////////////////// 1.8312 +///// Miscellaneous commands ///// 1.8313 +////////////////////////////////// 1.8314 + 1.8315 +///// The defaultParagraphSeparator command ///// 1.8316 +//@{ 1.8317 +commands.defaultparagraphseparator = { 1.8318 + action: function(value) { 1.8319 + // "Let value be converted to ASCII lowercase. If value is then equal 1.8320 + // to "p" or "div", set the context object's default single-line 1.8321 + // container name to value and return true. Otherwise, return false." 1.8322 + value = value.toLowerCase(); 1.8323 + if (value == "p" || value == "div") { 1.8324 + defaultSingleLineContainerName = value; 1.8325 + return true; 1.8326 + } 1.8327 + return false; 1.8328 + }, value: function() { 1.8329 + // "Return the context object's default single-line container name." 1.8330 + return defaultSingleLineContainerName; 1.8331 + }, 1.8332 +}; 1.8333 + 1.8334 +//@} 1.8335 +///// The selectAll command ///// 1.8336 +//@{ 1.8337 +commands.selectall = { 1.8338 + // Note, this ignores the whole globalRange/getActiveRange() thing and 1.8339 + // works with actual selections. Not suitable for autoimplementation.html. 1.8340 + action: function() { 1.8341 + // "Let target be the body element of the context object." 1.8342 + var target = document.body; 1.8343 + 1.8344 + // "If target is null, let target be the context object's 1.8345 + // documentElement." 1.8346 + if (!target) { 1.8347 + target = document.documentElement; 1.8348 + } 1.8349 + 1.8350 + // "If target is null, call getSelection() on the context object, and 1.8351 + // call removeAllRanges() on the result." 1.8352 + if (!target) { 1.8353 + getSelection().removeAllRanges(); 1.8354 + 1.8355 + // "Otherwise, call getSelection() on the context object, and call 1.8356 + // selectAllChildren(target) on the result." 1.8357 + } else { 1.8358 + getSelection().selectAllChildren(target); 1.8359 + } 1.8360 + 1.8361 + // "Return true." 1.8362 + return true; 1.8363 + } 1.8364 +}; 1.8365 + 1.8366 +//@} 1.8367 +///// The styleWithCSS command ///// 1.8368 +//@{ 1.8369 +commands.stylewithcss = { 1.8370 + action: function(value) { 1.8371 + // "If value is an ASCII case-insensitive match for the string 1.8372 + // "false", set the CSS styling flag to false. Otherwise, set the 1.8373 + // CSS styling flag to true. Either way, return true." 1.8374 + cssStylingFlag = String(value).toLowerCase() != "false"; 1.8375 + return true; 1.8376 + }, state: function() { return cssStylingFlag } 1.8377 +}; 1.8378 + 1.8379 +//@} 1.8380 +///// The useCSS command ///// 1.8381 +//@{ 1.8382 +commands.usecss = { 1.8383 + action: function(value) { 1.8384 + // "If value is an ASCII case-insensitive match for the string "false", 1.8385 + // set the CSS styling flag to true. Otherwise, set the CSS styling 1.8386 + // flag to false. Either way, return true." 1.8387 + cssStylingFlag = String(value).toLowerCase() == "false"; 1.8388 + return true; 1.8389 + } 1.8390 +}; 1.8391 +//@} 1.8392 + 1.8393 +// Some final setup 1.8394 +//@{ 1.8395 +(function() { 1.8396 +// Opera 11.50 doesn't implement Object.keys, so I have to make an explicit 1.8397 +// temporary, which means I need an extra closure to not leak the temporaries 1.8398 +// into the global namespace. >:( 1.8399 +var commandNames = []; 1.8400 +for (var command in commands) { 1.8401 + commandNames.push(command); 1.8402 +} 1.8403 +commandNames.forEach(function(command) { 1.8404 + // "If a command does not have a relevant CSS property specified, it 1.8405 + // defaults to null." 1.8406 + if (!("relevantCssProperty" in commands[command])) { 1.8407 + commands[command].relevantCssProperty = null; 1.8408 + } 1.8409 + 1.8410 + // "If a command has inline command activated values defined but nothing 1.8411 + // else defines when it is indeterminate, it is indeterminate if among 1.8412 + // formattable nodes effectively contained in the active range, there is at 1.8413 + // least one whose effective command value is one of the given values and 1.8414 + // at least one whose effective command value is not one of the given 1.8415 + // values." 1.8416 + if ("inlineCommandActivatedValues" in commands[command] 1.8417 + && !("indeterm" in commands[command])) { 1.8418 + commands[command].indeterm = function() { 1.8419 + if (!getActiveRange()) { 1.8420 + return false; 1.8421 + } 1.8422 + 1.8423 + var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) 1.8424 + .map(function(node) { return getEffectiveCommandValue(node, command) }); 1.8425 + 1.8426 + var matchingValues = values.filter(function(value) { 1.8427 + return commands[command].inlineCommandActivatedValues.indexOf(value) != -1; 1.8428 + }); 1.8429 + 1.8430 + return matchingValues.length >= 1 1.8431 + && values.length - matchingValues.length >= 1; 1.8432 + }; 1.8433 + } 1.8434 + 1.8435 + // "If a command has inline command activated values defined, its state is 1.8436 + // true if either no formattable node is effectively contained in the 1.8437 + // active range, and the active range's start node's effective command 1.8438 + // value is one of the given values; or if there is at least one 1.8439 + // formattable node effectively contained in the active range, and all of 1.8440 + // them have an effective command value equal to one of the given values." 1.8441 + if ("inlineCommandActivatedValues" in commands[command]) { 1.8442 + commands[command].state = function() { 1.8443 + if (!getActiveRange()) { 1.8444 + return false; 1.8445 + } 1.8446 + 1.8447 + var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); 1.8448 + 1.8449 + if (nodes.length == 0) { 1.8450 + return commands[command].inlineCommandActivatedValues 1.8451 + .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1; 1.8452 + } else { 1.8453 + return nodes.every(function(node) { 1.8454 + return commands[command].inlineCommandActivatedValues 1.8455 + .indexOf(getEffectiveCommandValue(node, command)) != -1; 1.8456 + }); 1.8457 + } 1.8458 + }; 1.8459 + } 1.8460 + 1.8461 + // "If a command is a standard inline value command, it is indeterminate if 1.8462 + // among formattable nodes that are effectively contained in the active 1.8463 + // range, there are two that have distinct effective command values. Its 1.8464 + // value is the effective command value of the first formattable node that 1.8465 + // is effectively contained in the active range; or if there is no such 1.8466 + // node, the effective command value of the active range's start node; or 1.8467 + // if that is null, the empty string." 1.8468 + if ("standardInlineValueCommand" in commands[command]) { 1.8469 + commands[command].indeterm = function() { 1.8470 + if (!getActiveRange()) { 1.8471 + return false; 1.8472 + } 1.8473 + 1.8474 + var values = getAllEffectivelyContainedNodes(getActiveRange()) 1.8475 + .filter(isFormattableNode) 1.8476 + .map(function(node) { return getEffectiveCommandValue(node, command) }); 1.8477 + for (var i = 1; i < values.length; i++) { 1.8478 + if (values[i] != values[i - 1]) { 1.8479 + return true; 1.8480 + } 1.8481 + } 1.8482 + return false; 1.8483 + }; 1.8484 + 1.8485 + commands[command].value = function() { 1.8486 + if (!getActiveRange()) { 1.8487 + return ""; 1.8488 + } 1.8489 + 1.8490 + var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; 1.8491 + 1.8492 + if (typeof refNode == "undefined") { 1.8493 + refNode = getActiveRange().startContainer; 1.8494 + } 1.8495 + 1.8496 + var ret = getEffectiveCommandValue(refNode, command); 1.8497 + if (ret === null) { 1.8498 + return ""; 1.8499 + } 1.8500 + return ret; 1.8501 + }; 1.8502 + } 1.8503 + 1.8504 + // "If a command preserves overrides, then before taking its action, the 1.8505 + // user agent must record current overrides. After taking the action, if 1.8506 + // the active range is collapsed, it must restore states and values from 1.8507 + // the recorded list." 1.8508 + if ("preservesOverrides" in commands[command]) { 1.8509 + var oldAction = commands[command].action; 1.8510 + 1.8511 + commands[command].action = function(value) { 1.8512 + var overrides = recordCurrentOverrides(); 1.8513 + var ret = oldAction(value); 1.8514 + if (getActiveRange().collapsed) { 1.8515 + restoreStatesAndValues(overrides); 1.8516 + } 1.8517 + return ret; 1.8518 + }; 1.8519 + } 1.8520 +}); 1.8521 +})(); 1.8522 +//@} 1.8523 + 1.8524 +// vim: foldmarker=@{,@} foldmethod=marker