OLD | NEW |
(Empty) | |
| 1 "use strict"; |
| 2 |
| 3 var htmlNamespace = "http://www.w3.org/1999/xhtml"; |
| 4 |
| 5 var cssStylingFlag = false; |
| 6 |
| 7 var defaultSingleLineContainerName = "div"; |
| 8 |
| 9 // This is bad :( |
| 10 var globalRange = null; |
| 11 |
| 12 // Commands are stored in a dictionary where we call their actions and such |
| 13 var commands = {}; |
| 14 |
| 15 /////////////////////////////////////////////////////////////////////////////// |
| 16 ////////////////////////////// Utility functions ////////////////////////////// |
| 17 /////////////////////////////////////////////////////////////////////////////// |
| 18 //@{ |
| 19 |
| 20 function nextNode(node) { |
| 21 if (node.hasChildNodes()) { |
| 22 return node.firstChild; |
| 23 } |
| 24 return nextNodeDescendants(node); |
| 25 } |
| 26 |
| 27 function previousNode(node) { |
| 28 if (node.previousSibling) { |
| 29 node = node.previousSibling; |
| 30 while (node.hasChildNodes()) { |
| 31 node = node.lastChild; |
| 32 } |
| 33 return node; |
| 34 } |
| 35 if (node.parentNode |
| 36 && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
| 37 return node.parentNode; |
| 38 } |
| 39 return null; |
| 40 } |
| 41 |
| 42 function nextNodeDescendants(node) { |
| 43 while (node && !node.nextSibling) { |
| 44 node = node.parentNode; |
| 45 } |
| 46 if (!node) { |
| 47 return null; |
| 48 } |
| 49 return node.nextSibling; |
| 50 } |
| 51 |
| 52 /** |
| 53 * Returns true if ancestor is an ancestor of descendant, false otherwise. |
| 54 */ |
| 55 function isAncestor(ancestor, descendant) { |
| 56 return ancestor |
| 57 && descendant |
| 58 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_
POSITION_CONTAINED_BY); |
| 59 } |
| 60 |
| 61 /** |
| 62 * Returns true if ancestor is an ancestor of or equal to descendant, false |
| 63 * otherwise. |
| 64 */ |
| 65 function isAncestorContainer(ancestor, descendant) { |
| 66 return (ancestor || descendant) |
| 67 && (ancestor == descendant || isAncestor(ancestor, descendant)); |
| 68 } |
| 69 |
| 70 /** |
| 71 * Returns true if descendant is a descendant of ancestor, false otherwise. |
| 72 */ |
| 73 function isDescendant(descendant, ancestor) { |
| 74 return ancestor |
| 75 && descendant |
| 76 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_
POSITION_CONTAINED_BY); |
| 77 } |
| 78 |
| 79 /** |
| 80 * Returns true if node1 is before node2 in tree order, false otherwise. |
| 81 */ |
| 82 function isBefore(node1, node2) { |
| 83 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION
_FOLLOWING); |
| 84 } |
| 85 |
| 86 /** |
| 87 * Returns true if node1 is after node2 in tree order, false otherwise. |
| 88 */ |
| 89 function isAfter(node1, node2) { |
| 90 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION
_PRECEDING); |
| 91 } |
| 92 |
| 93 function getAncestors(node) { |
| 94 var ancestors = []; |
| 95 while (node.parentNode) { |
| 96 ancestors.unshift(node.parentNode); |
| 97 node = node.parentNode; |
| 98 } |
| 99 return ancestors; |
| 100 } |
| 101 |
| 102 function getInclusiveAncestors(node) { |
| 103 return getAncestors(node).concat(node); |
| 104 } |
| 105 |
| 106 function getDescendants(node) { |
| 107 var descendants = []; |
| 108 var stop = nextNodeDescendants(node); |
| 109 while ((node = nextNode(node)) |
| 110 && node != stop) { |
| 111 descendants.push(node); |
| 112 } |
| 113 return descendants; |
| 114 } |
| 115 |
| 116 function getInclusiveDescendants(node) { |
| 117 return [node].concat(getDescendants(node)); |
| 118 } |
| 119 |
| 120 function convertProperty(property) { |
| 121 // Special-case for now |
| 122 var map = { |
| 123 "fontFamily": "font-family", |
| 124 "fontSize": "font-size", |
| 125 "fontStyle": "font-style", |
| 126 "fontWeight": "font-weight", |
| 127 "textDecoration": "text-decoration", |
| 128 }; |
| 129 if (typeof map[property] != "undefined") { |
| 130 return map[property]; |
| 131 } |
| 132 |
| 133 return property; |
| 134 } |
| 135 |
| 136 // Return the <font size=X> value for the given CSS size, or undefined if there |
| 137 // is none. |
| 138 function cssSizeToLegacy(cssVal) { |
| 139 return { |
| 140 "x-small": 1, |
| 141 "small": 2, |
| 142 "medium": 3, |
| 143 "large": 4, |
| 144 "x-large": 5, |
| 145 "xx-large": 6, |
| 146 "xxx-large": 7 |
| 147 }[cssVal]; |
| 148 } |
| 149 |
| 150 // Return the CSS size given a legacy size. |
| 151 function legacySizeToCss(legacyVal) { |
| 152 return { |
| 153 1: "x-small", |
| 154 2: "small", |
| 155 3: "medium", |
| 156 4: "large", |
| 157 5: "x-large", |
| 158 6: "xx-large", |
| 159 7: "xxx-large", |
| 160 }[legacyVal]; |
| 161 } |
| 162 |
| 163 // Opera 11 puts HTML elements in the null namespace, it seems. |
| 164 function isHtmlNamespace(ns) { |
| 165 return ns === null |
| 166 || ns === htmlNamespace; |
| 167 } |
| 168 |
| 169 // "the directionality" from HTML. I don't bother caring about non-HTML |
| 170 // elements. |
| 171 // |
| 172 // "The directionality of an element is either 'ltr' or 'rtl', and is |
| 173 // determined as per the first appropriate set of steps from the following |
| 174 // list:" |
| 175 function getDirectionality(element) { |
| 176 // "If the element's dir attribute is in the ltr state |
| 177 // The directionality of the element is 'ltr'." |
| 178 if (element.dir == "ltr") { |
| 179 return "ltr"; |
| 180 } |
| 181 |
| 182 // "If the element's dir attribute is in the rtl state |
| 183 // The directionality of the element is 'rtl'." |
| 184 if (element.dir == "rtl") { |
| 185 return "rtl"; |
| 186 } |
| 187 |
| 188 // "If the element's dir attribute is in the auto state |
| 189 // "If the element is a bdi element and the dir attribute is not in a |
| 190 // defined state (i.e. it is not present or has an invalid value) |
| 191 // [lots of complicated stuff] |
| 192 // |
| 193 // Skip this, since no browser implements it anyway. |
| 194 |
| 195 // "If the element is a root element and the dir attribute is not in a |
| 196 // defined state (i.e. it is not present or has an invalid value) |
| 197 // The directionality of the element is 'ltr'." |
| 198 if (!isHtmlElement(element.parentNode)) { |
| 199 return "ltr"; |
| 200 } |
| 201 |
| 202 // "If the element has a parent element and the dir attribute is not in a |
| 203 // defined state (i.e. it is not present or has an invalid value) |
| 204 // The directionality of the element is the same as the element's |
| 205 // parent element's directionality." |
| 206 return getDirectionality(element.parentNode); |
| 207 } |
| 208 |
| 209 //@} |
| 210 |
| 211 /////////////////////////////////////////////////////////////////////////////// |
| 212 ///////////////////////////// DOM Range functions ///////////////////////////// |
| 213 /////////////////////////////////////////////////////////////////////////////// |
| 214 //@{ |
| 215 |
| 216 function getNodeIndex(node) { |
| 217 var ret = 0; |
| 218 while (node.previousSibling) { |
| 219 ret++; |
| 220 node = node.previousSibling; |
| 221 } |
| 222 return ret; |
| 223 } |
| 224 |
| 225 // "The length of a Node node is the following, depending on node: |
| 226 // |
| 227 // ProcessingInstruction |
| 228 // DocumentType |
| 229 // Always 0. |
| 230 // Text |
| 231 // Comment |
| 232 // node's length. |
| 233 // Any other node |
| 234 // node's childNodes's length." |
| 235 function getNodeLength(node) { |
| 236 switch (node.nodeType) { |
| 237 case Node.PROCESSING_INSTRUCTION_NODE: |
| 238 case Node.DOCUMENT_TYPE_NODE: |
| 239 return 0; |
| 240 |
| 241 case Node.TEXT_NODE: |
| 242 case Node.COMMENT_NODE: |
| 243 return node.length; |
| 244 |
| 245 default: |
| 246 return node.childNodes.length; |
| 247 } |
| 248 } |
| 249 |
| 250 /** |
| 251 * The position of two boundary points relative to one another, as defined by |
| 252 * DOM Range. |
| 253 */ |
| 254 function getPosition(nodeA, offsetA, nodeB, offsetB) { |
| 255 // "If node A is the same as node B, return equal if offset A equals offset |
| 256 // B, before if offset A is less than offset B, and after if offset A is |
| 257 // greater than offset B." |
| 258 if (nodeA == nodeB) { |
| 259 if (offsetA == offsetB) { |
| 260 return "equal"; |
| 261 } |
| 262 if (offsetA < offsetB) { |
| 263 return "before"; |
| 264 } |
| 265 if (offsetA > offsetB) { |
| 266 return "after"; |
| 267 } |
| 268 } |
| 269 |
| 270 // "If node A is after node B in tree order, compute the position of (node |
| 271 // B, offset B) relative to (node A, offset A). If it is before, return |
| 272 // after. If it is after, return before." |
| 273 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING)
{ |
| 274 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); |
| 275 if (pos == "before") { |
| 276 return "after"; |
| 277 } |
| 278 if (pos == "after") { |
| 279 return "before"; |
| 280 } |
| 281 } |
| 282 |
| 283 // "If node A is an ancestor of node B:" |
| 284 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS)
{ |
| 285 // "Let child equal node B." |
| 286 var child = nodeB; |
| 287 |
| 288 // "While child is not a child of node A, set child to its parent." |
| 289 while (child.parentNode != nodeA) { |
| 290 child = child.parentNode; |
| 291 } |
| 292 |
| 293 // "If the index of child is less than offset A, return after." |
| 294 if (getNodeIndex(child) < offsetA) { |
| 295 return "after"; |
| 296 } |
| 297 } |
| 298 |
| 299 // "Return before." |
| 300 return "before"; |
| 301 } |
| 302 |
| 303 /** |
| 304 * Returns the furthest ancestor of a Node as defined by DOM Range. |
| 305 */ |
| 306 function getFurthestAncestor(node) { |
| 307 var root = node; |
| 308 while (root.parentNode != null) { |
| 309 root = root.parentNode; |
| 310 } |
| 311 return root; |
| 312 } |
| 313 |
| 314 /** |
| 315 * "contained" as defined by DOM Range: "A Node node is contained in a range |
| 316 * range if node's furthest ancestor is the same as range's root, and (node, 0) |
| 317 * is after range's start, and (node, length of node) is before range's end." |
| 318 */ |
| 319 function isContained(node, range) { |
| 320 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); |
| 321 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.
endOffset); |
| 322 |
| 323 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer
) |
| 324 && pos1 == "after" |
| 325 && pos2 == "before"; |
| 326 } |
| 327 |
| 328 /** |
| 329 * Return all nodes contained in range that the provided function returns true |
| 330 * for, omitting any with an ancestor already being returned. |
| 331 */ |
| 332 function getContainedNodes(range, condition) { |
| 333 if (typeof condition == "undefined") { |
| 334 condition = function() { return true }; |
| 335 } |
| 336 var node = range.startContainer; |
| 337 if (node.hasChildNodes() |
| 338 && range.startOffset < node.childNodes.length) { |
| 339 // A child is contained |
| 340 node = node.childNodes[range.startOffset]; |
| 341 } else if (range.startOffset == getNodeLength(node)) { |
| 342 // No descendant can be contained |
| 343 node = nextNodeDescendants(node); |
| 344 } else { |
| 345 // No children; this node at least can't be contained |
| 346 node = nextNode(node); |
| 347 } |
| 348 |
| 349 var stop = range.endContainer; |
| 350 if (stop.hasChildNodes() |
| 351 && range.endOffset < stop.childNodes.length) { |
| 352 // The node after the last contained node is a child |
| 353 stop = stop.childNodes[range.endOffset]; |
| 354 } else { |
| 355 // This node and/or some of its children might be contained |
| 356 stop = nextNodeDescendants(stop); |
| 357 } |
| 358 |
| 359 var nodeList = []; |
| 360 while (isBefore(node, stop)) { |
| 361 if (isContained(node, range) |
| 362 && condition(node)) { |
| 363 nodeList.push(node); |
| 364 node = nextNodeDescendants(node); |
| 365 continue; |
| 366 } |
| 367 node = nextNode(node); |
| 368 } |
| 369 return nodeList; |
| 370 } |
| 371 |
| 372 /** |
| 373 * As above, but includes nodes with an ancestor that's already been returned. |
| 374 */ |
| 375 function getAllContainedNodes(range, condition) { |
| 376 if (typeof condition == "undefined") { |
| 377 condition = function() { return true }; |
| 378 } |
| 379 var node = range.startContainer; |
| 380 if (node.hasChildNodes() |
| 381 && range.startOffset < node.childNodes.length) { |
| 382 // A child is contained |
| 383 node = node.childNodes[range.startOffset]; |
| 384 } else if (range.startOffset == getNodeLength(node)) { |
| 385 // No descendant can be contained |
| 386 node = nextNodeDescendants(node); |
| 387 } else { |
| 388 // No children; this node at least can't be contained |
| 389 node = nextNode(node); |
| 390 } |
| 391 |
| 392 var stop = range.endContainer; |
| 393 if (stop.hasChildNodes() |
| 394 && range.endOffset < stop.childNodes.length) { |
| 395 // The node after the last contained node is a child |
| 396 stop = stop.childNodes[range.endOffset]; |
| 397 } else { |
| 398 // This node and/or some of its children might be contained |
| 399 stop = nextNodeDescendants(stop); |
| 400 } |
| 401 |
| 402 var nodeList = []; |
| 403 while (isBefore(node, stop)) { |
| 404 if (isContained(node, range) |
| 405 && condition(node)) { |
| 406 nodeList.push(node); |
| 407 } |
| 408 node = nextNode(node); |
| 409 } |
| 410 return nodeList; |
| 411 } |
| 412 |
| 413 // Returns either null, or something of the form rgb(x, y, z), or something of |
| 414 // the form rgb(x, y, z, w) with w != 0. |
| 415 function normalizeColor(color) { |
| 416 if (color.toLowerCase() == "currentcolor") { |
| 417 return null; |
| 418 } |
| 419 |
| 420 if (normalizeColor.resultCache === undefined) { |
| 421 normalizeColor.resultCache = {}; |
| 422 } |
| 423 |
| 424 if (normalizeColor.resultCache[color] !== undefined) { |
| 425 return normalizeColor.resultCache[color]; |
| 426 } |
| 427 |
| 428 var originalColor = color; |
| 429 |
| 430 var outerSpan = document.createElement("span"); |
| 431 document.body.appendChild(outerSpan); |
| 432 outerSpan.style.color = "black"; |
| 433 |
| 434 var innerSpan = document.createElement("span"); |
| 435 outerSpan.appendChild(innerSpan); |
| 436 innerSpan.style.color = color; |
| 437 color = getComputedStyle(innerSpan).color; |
| 438 |
| 439 if (color == "rgb(0, 0, 0)") { |
| 440 // Maybe it's really black, maybe it's invalid. |
| 441 outerSpan.color = "white"; |
| 442 color = getComputedStyle(innerSpan).color; |
| 443 if (color != "rgb(0, 0, 0)") { |
| 444 return normalizeColor.resultCache[originalColor] = null; |
| 445 } |
| 446 } |
| 447 |
| 448 document.body.removeChild(outerSpan); |
| 449 |
| 450 // I rely on the fact that browsers generally provide consistent syntax for |
| 451 // getComputedStyle(), although it's not standardized. There are only |
| 452 // three exceptions I found: |
| 453 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { |
| 454 // IE10PP2 seems to do this sometimes. |
| 455 return normalizeColor.resultCache[originalColor] = |
| 456 color.replace("rgba", "rgb").replace(", 1)", ")"); |
| 457 } |
| 458 if (color == "transparent") { |
| 459 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if |
| 460 // the specified value is "transparent". |
| 461 return normalizeColor.resultCache[originalColor] = |
| 462 "rgba(0, 0, 0, 0)"; |
| 463 } |
| 464 // Chrome 15 dev adds way too many significant figures. This isn't a full |
| 465 // fix, it just fixes one case that comes up in tests. |
| 466 color = color.replace(/, 0.496094\)$/, ", 0.5)"); |
| 467 return normalizeColor.resultCache[originalColor] = color; |
| 468 } |
| 469 |
| 470 // Returns either null, or something of the form #xxxxxx. |
| 471 function parseSimpleColor(color) { |
| 472 color = normalizeColor(color); |
| 473 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); |
| 474 if (matches) { |
| 475 return "#" |
| 476 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&") |
| 477 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&") |
| 478 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&"); |
| 479 } |
| 480 return null; |
| 481 } |
| 482 |
| 483 //@} |
| 484 |
| 485 ////////////////////////////////////////////////////////////////////////////// |
| 486 /////////////////////////// Edit command functions /////////////////////////// |
| 487 ////////////////////////////////////////////////////////////////////////////// |
| 488 |
| 489 ///////////////////////////////////////////////// |
| 490 ///// Methods of the HTMLDocument interface ///// |
| 491 ///////////////////////////////////////////////// |
| 492 //@{ |
| 493 |
| 494 var executionStackDepth = 0; |
| 495 |
| 496 // Helper function for common behavior. |
| 497 function editCommandMethod(command, range, callback) { |
| 498 // Set up our global range magic, but only if we're the outermost function |
| 499 if (executionStackDepth == 0 && typeof range != "undefined") { |
| 500 globalRange = range; |
| 501 } else if (executionStackDepth == 0) { |
| 502 globalRange = null; |
| 503 globalRange = getActiveRange(); |
| 504 } |
| 505 |
| 506 executionStackDepth++; |
| 507 try { |
| 508 var ret = callback(); |
| 509 } catch(e) { |
| 510 executionStackDepth--; |
| 511 throw e; |
| 512 } |
| 513 executionStackDepth--; |
| 514 return ret; |
| 515 } |
| 516 |
| 517 function myExecCommand(command, showUi, value, range) { |
| 518 // "All of these methods must treat their command argument ASCII |
| 519 // case-insensitively." |
| 520 command = command.toLowerCase(); |
| 521 |
| 522 // "If only one argument was provided, let show UI be false." |
| 523 // |
| 524 // If range was passed, I can't actually detect how many args were passed |
| 525 // . . . |
| 526 if (arguments.length == 1 |
| 527 || (arguments.length >=4 && typeof showUi == "undefined")) { |
| 528 showUi = false; |
| 529 } |
| 530 |
| 531 // "If only one or two arguments were provided, let value be the empty |
| 532 // string." |
| 533 if (arguments.length <= 2 |
| 534 || (arguments.length >=4 && typeof value == "undefined")) { |
| 535 value = ""; |
| 536 } |
| 537 |
| 538 return editCommandMethod(command, range, (function(command, showUi, value) {
return function() { |
| 539 // "If command is not supported or not enabled, return false." |
| 540 if (!(command in commands) || !myQueryCommandEnabled(command)) { |
| 541 return false; |
| 542 } |
| 543 |
| 544 // "Take the action for command, passing value to the instructions as an |
| 545 // argument." |
| 546 var ret = commands[command].action(value); |
| 547 |
| 548 // Check for bugs |
| 549 if (ret !== true && ret !== false) { |
| 550 throw "execCommand() didn't return true or false: " + ret; |
| 551 } |
| 552 |
| 553 // "If the previous step returned false, return false." |
| 554 if (ret === false) { |
| 555 return false; |
| 556 } |
| 557 |
| 558 // "Return true." |
| 559 return true; |
| 560 }})(command, showUi, value)); |
| 561 } |
| 562 |
| 563 function myQueryCommandEnabled(command, range) { |
| 564 // "All of these methods must treat their command argument ASCII |
| 565 // case-insensitively." |
| 566 command = command.toLowerCase(); |
| 567 |
| 568 return editCommandMethod(command, range, (function(command) { return functio
n() { |
| 569 // "Return true if command is both supported and enabled, false |
| 570 // otherwise." |
| 571 if (!(command in commands)) { |
| 572 return false; |
| 573 } |
| 574 |
| 575 // "Among commands defined in this specification, those listed in |
| 576 // Miscellaneous commands are always enabled, except for the cut |
| 577 // command and the paste command. The other commands defined here are |
| 578 // enabled if the active range is not null, its start node is either |
| 579 // editable or an editing host, its end node is either editable or an |
| 580 // editing host, and there is some editing host that is an inclusive |
| 581 // ancestor of both its start node and its end node." |
| 582 return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss"
, |
| 583 "usecss"].indexOf(command) != -1 |
| 584 || ( |
| 585 getActiveRange() !== null |
| 586 && (isEditable(getActiveRange().startContainer) || isEditingHost
(getActiveRange().startContainer)) |
| 587 && (isEditable(getActiveRange().endContainer) || isEditingHost(g
etActiveRange().endContainer)) |
| 588 && (getInclusiveAncestors(getActiveRange().commonAncestorContain
er).some(isEditingHost)) |
| 589 ); |
| 590 }})(command)); |
| 591 } |
| 592 |
| 593 function myQueryCommandIndeterm(command, range) { |
| 594 // "All of these methods must treat their command argument ASCII |
| 595 // case-insensitively." |
| 596 command = command.toLowerCase(); |
| 597 |
| 598 return editCommandMethod(command, range, (function(command) { return functio
n() { |
| 599 // "If command is not supported or has no indeterminacy, return false." |
| 600 if (!(command in commands) || !("indeterm" in commands[command])) { |
| 601 return false; |
| 602 } |
| 603 |
| 604 // "Return true if command is indeterminate, otherwise false." |
| 605 return commands[command].indeterm(); |
| 606 }})(command)); |
| 607 } |
| 608 |
| 609 function myQueryCommandState(command, range) { |
| 610 // "All of these methods must treat their command argument ASCII |
| 611 // case-insensitively." |
| 612 command = command.toLowerCase(); |
| 613 |
| 614 return editCommandMethod(command, range, (function(command) { return functio
n() { |
| 615 // "If command is not supported or has no state, return false." |
| 616 if (!(command in commands) || !("state" in commands[command])) { |
| 617 return false; |
| 618 } |
| 619 |
| 620 // "If the state override for command is set, return it." |
| 621 if (typeof getStateOverride(command) != "undefined") { |
| 622 return getStateOverride(command); |
| 623 } |
| 624 |
| 625 // "Return true if command's state is true, otherwise false." |
| 626 return commands[command].state(); |
| 627 }})(command)); |
| 628 } |
| 629 |
| 630 // "When the queryCommandSupported(command) method on the HTMLDocument |
| 631 // interface is invoked, the user agent must return true if command is |
| 632 // supported, and false otherwise." |
| 633 function myQueryCommandSupported(command) { |
| 634 // "All of these methods must treat their command argument ASCII |
| 635 // case-insensitively." |
| 636 command = command.toLowerCase(); |
| 637 |
| 638 return command in commands; |
| 639 } |
| 640 |
| 641 function myQueryCommandValue(command, range) { |
| 642 // "All of these methods must treat their command argument ASCII |
| 643 // case-insensitively." |
| 644 command = command.toLowerCase(); |
| 645 |
| 646 return editCommandMethod(command, range, function() { |
| 647 // "If command is not supported or has no value, return the empty string
." |
| 648 if (!(command in commands) || !("value" in commands[command])) { |
| 649 return ""; |
| 650 } |
| 651 |
| 652 // "If command is "fontSize" and its value override is set, convert the |
| 653 // value override to an integer number of pixels and return the legacy |
| 654 // font size for the result." |
| 655 if (command == "fontsize" |
| 656 && getValueOverride("fontsize") !== undefined) { |
| 657 return getLegacyFontSize(getValueOverride("fontsize")); |
| 658 } |
| 659 |
| 660 // "If the value override for command is set, return it." |
| 661 if (typeof getValueOverride(command) != "undefined") { |
| 662 return getValueOverride(command); |
| 663 } |
| 664 |
| 665 // "Return command's value." |
| 666 return commands[command].value(); |
| 667 }); |
| 668 } |
| 669 //@} |
| 670 |
| 671 ////////////////////////////// |
| 672 ///// Common definitions ///// |
| 673 ////////////////////////////// |
| 674 //@{ |
| 675 |
| 676 // "An HTML element is an Element whose namespace is the HTML namespace." |
| 677 // |
| 678 // I allow an extra argument to more easily check whether something is a |
| 679 // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays |
| 680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul. |
| 681 function isHtmlElement(node, tags) { |
| 682 if (typeof tags == "string") { |
| 683 tags = [tags]; |
| 684 } |
| 685 if (typeof tags == "object") { |
| 686 tags = tags.map(function(tag) { return tag.toUpperCase() }); |
| 687 } |
| 688 return node |
| 689 && node.nodeType == Node.ELEMENT_NODE |
| 690 && isHtmlNamespace(node.namespaceURI) |
| 691 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1); |
| 692 } |
| 693 |
| 694 // "A prohibited paragraph child name is "address", "article", "aside", |
| 695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
| 696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
| 697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
| 698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
| 699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or |
| 700 // "xmp"." |
| 701 var prohibitedParagraphChildNames = ["address", "article", "aside", |
| 702 "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
| 703 "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
| 704 "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
| 705 "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
| 706 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", |
| 707 "xmp"]; |
| 708 |
| 709 // "A prohibited paragraph child is an HTML element whose local name is a |
| 710 // prohibited paragraph child name." |
| 711 function isProhibitedParagraphChild(node) { |
| 712 return isHtmlElement(node, prohibitedParagraphChildNames); |
| 713 } |
| 714 |
| 715 // "A block node is either an Element whose "display" property does not have |
| 716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a |
| 717 // Document, or a DocumentFragment." |
| 718 function isBlockNode(node) { |
| 719 return node |
| 720 && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "i
nline-table", "none"].indexOf(getComputedStyle(node).display) == -1) |
| 721 || node.nodeType == Node.DOCUMENT_NODE |
| 722 || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE); |
| 723 } |
| 724 |
| 725 // "An inline node is a node that is not a block node." |
| 726 function isInlineNode(node) { |
| 727 return node && !isBlockNode(node); |
| 728 } |
| 729 |
| 730 // "An editing host is a node that is either an HTML element with a |
| 731 // contenteditable attribute set to the true state, or the HTML element child |
| 732 // of a Document whose designMode is enabled." |
| 733 function isEditingHost(node) { |
| 734 return node |
| 735 && isHtmlElement(node) |
| 736 && (node.contentEditable == "true" |
| 737 || (node.parentNode |
| 738 && node.parentNode.nodeType == Node.DOCUMENT_NODE |
| 739 && node.parentNode.designMode == "on")); |
| 740 } |
| 741 |
| 742 // "Something is editable if it is a node; it is not an editing host; it does |
| 743 // not have a contenteditable attribute set to the false state; its parent is |
| 744 // an editing host or editable; and either it is an HTML element, or it is an |
| 745 // svg or math element, or it is not an Element and its parent is an HTML |
| 746 // element." |
| 747 function isEditable(node) { |
| 748 return node |
| 749 && !isEditingHost(node) |
| 750 && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false
") |
| 751 && (isEditingHost(node.parentNode) || isEditable(node.parentNode)) |
| 752 && (isHtmlElement(node) |
| 753 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://w
ww.w3.org/2000/svg" && node.localName == "svg") |
| 754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://w
ww.w3.org/1998/Math/MathML" && node.localName == "math") |
| 755 || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)
)); |
| 756 } |
| 757 |
| 758 // Helper function, not defined in the spec |
| 759 function hasEditableDescendants(node) { |
| 760 for (var i = 0; i < node.childNodes.length; i++) { |
| 761 if (isEditable(node.childNodes[i]) |
| 762 || hasEditableDescendants(node.childNodes[i])) { |
| 763 return true; |
| 764 } |
| 765 } |
| 766 return false; |
| 767 } |
| 768 |
| 769 // "The editing host of node is null if node is neither editable nor an editing |
| 770 // host; node itself, if node is an editing host; or the nearest ancestor of |
| 771 // node that is an editing host, if node is editable." |
| 772 function getEditingHostOf(node) { |
| 773 if (isEditingHost(node)) { |
| 774 return node; |
| 775 } else if (isEditable(node)) { |
| 776 var ancestor = node.parentNode; |
| 777 while (!isEditingHost(ancestor)) { |
| 778 ancestor = ancestor.parentNode; |
| 779 } |
| 780 return ancestor; |
| 781 } else { |
| 782 return null; |
| 783 } |
| 784 } |
| 785 |
| 786 // "Two nodes are in the same editing host if the editing host of the first is |
| 787 // non-null and the same as the editing host of the second." |
| 788 function inSameEditingHost(node1, node2) { |
| 789 return getEditingHostOf(node1) |
| 790 && getEditingHostOf(node1) == getEditingHostOf(node2); |
| 791 } |
| 792 |
| 793 // "A collapsed line break is a br that begins a line box which has nothing |
| 794 // else in it, and therefore has zero height." |
| 795 function isCollapsedLineBreak(br) { |
| 796 if (!isHtmlElement(br, "br")) { |
| 797 return false; |
| 798 } |
| 799 |
| 800 // Add a zwsp after it and see if that changes the height of the nearest |
| 801 // non-inline parent. Note: this is not actually reliable, because the |
| 802 // parent might have a fixed height or something. |
| 803 var ref = br.parentNode; |
| 804 while (getComputedStyle(ref).display == "inline") { |
| 805 ref = ref.parentNode; |
| 806 } |
| 807 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
| 808 ref.style.height = "auto"; |
| 809 ref.style.maxHeight = "none"; |
| 810 ref.style.minHeight = "0"; |
| 811 var space = document.createTextNode("\u200b"); |
| 812 var origHeight = ref.offsetHeight; |
| 813 if (origHeight == 0) { |
| 814 throw "isCollapsedLineBreak: original height is zero, bug?"; |
| 815 } |
| 816 br.parentNode.insertBefore(space, br.nextSibling); |
| 817 var finalHeight = ref.offsetHeight; |
| 818 space.parentNode.removeChild(space); |
| 819 if (refStyle === null) { |
| 820 // Without the setAttribute() line, removeAttribute() doesn't work in |
| 821 // Chrome 14 dev. I have no idea why. |
| 822 ref.setAttribute("style", ""); |
| 823 ref.removeAttribute("style"); |
| 824 } else { |
| 825 ref.setAttribute("style", refStyle); |
| 826 } |
| 827 |
| 828 // Allow some leeway in case the zwsp didn't create a whole new line, but |
| 829 // only made an existing line slightly higher. Firefox 6.0a2 shows this |
| 830 // behavior when the first line is bold. |
| 831 return origHeight < finalHeight - 5; |
| 832 } |
| 833 |
| 834 // "An extraneous line break is a br that has no visual effect, in that |
| 835 // removing it from the DOM would not change layout, except that a br that is |
| 836 // the sole child of an li is not extraneous." |
| 837 // |
| 838 // FIXME: This doesn't work in IE, since IE ignores display: none in |
| 839 // contenteditable. |
| 840 function isExtraneousLineBreak(br) { |
| 841 if (!isHtmlElement(br, "br")) { |
| 842 return false; |
| 843 } |
| 844 |
| 845 if (isHtmlElement(br.parentNode, "li") |
| 846 && br.parentNode.childNodes.length == 1) { |
| 847 return false; |
| 848 } |
| 849 |
| 850 // Make the line break disappear and see if that changes the block's |
| 851 // height. Yes, this is an absurd hack. We have to reset height etc. on |
| 852 // the reference node because otherwise its height won't change if it's not |
| 853 // auto. |
| 854 var ref = br.parentNode; |
| 855 while (getComputedStyle(ref).display == "inline") { |
| 856 ref = ref.parentNode; |
| 857 } |
| 858 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
| 859 ref.style.height = "auto"; |
| 860 ref.style.maxHeight = "none"; |
| 861 ref.style.minHeight = "0"; |
| 862 var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null; |
| 863 var origHeight = ref.offsetHeight; |
| 864 if (origHeight == 0) { |
| 865 throw "isExtraneousLineBreak: original height is zero, bug?"; |
| 866 } |
| 867 br.setAttribute("style", "display:none"); |
| 868 var finalHeight = ref.offsetHeight; |
| 869 if (refStyle === null) { |
| 870 // Without the setAttribute() line, removeAttribute() doesn't work in |
| 871 // Chrome 14 dev. I have no idea why. |
| 872 ref.setAttribute("style", ""); |
| 873 ref.removeAttribute("style"); |
| 874 } else { |
| 875 ref.setAttribute("style", refStyle); |
| 876 } |
| 877 if (brStyle === null) { |
| 878 br.removeAttribute("style"); |
| 879 } else { |
| 880 br.setAttribute("style", brStyle); |
| 881 } |
| 882 |
| 883 return origHeight == finalHeight; |
| 884 } |
| 885 |
| 886 // "A whitespace node is either a Text node whose data is the empty string; or |
| 887 // a Text node whose data consists only of one or more tabs (0x0009), line |
| 888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose |
| 889 // parent is an Element whose resolved value for "white-space" is "normal" or |
| 890 // "nowrap"; or a Text node whose data consists only of one or more tabs |
| 891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose |
| 892 // parent is an Element whose resolved value for "white-space" is "pre-line"." |
| 893 function isWhitespaceNode(node) { |
| 894 return node |
| 895 && node.nodeType == Node.TEXT_NODE |
| 896 && (node.data == "" |
| 897 || ( |
| 898 /^[\t\n\r ]+$/.test(node.data) |
| 899 && node.parentNode |
| 900 && node.parentNode.nodeType == Node.ELEMENT_NODE |
| 901 && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).wh
iteSpace) != -1 |
| 902 ) || ( |
| 903 /^[\t\r ]+$/.test(node.data) |
| 904 && node.parentNode |
| 905 && node.parentNode.nodeType == Node.ELEMENT_NODE |
| 906 && getComputedStyle(node.parentNode).whiteSpace == "pre-line" |
| 907 )); |
| 908 } |
| 909 |
| 910 // "node is a collapsed whitespace node if the following algorithm returns |
| 911 // true:" |
| 912 function isCollapsedWhitespaceNode(node) { |
| 913 // "If node is not a whitespace node, return false." |
| 914 if (!isWhitespaceNode(node)) { |
| 915 return false; |
| 916 } |
| 917 |
| 918 // "If node's data is the empty string, return true." |
| 919 if (node.data == "") { |
| 920 return true; |
| 921 } |
| 922 |
| 923 // "Let ancestor be node's parent." |
| 924 var ancestor = node.parentNode; |
| 925 |
| 926 // "If ancestor is null, return true." |
| 927 if (!ancestor) { |
| 928 return true; |
| 929 } |
| 930 |
| 931 // "If the "display" property of some ancestor of node has resolved value |
| 932 // "none", return true." |
| 933 if (getAncestors(node).some(function(ancestor) { |
| 934 return ancestor.nodeType == Node.ELEMENT_NODE |
| 935 && getComputedStyle(ancestor).display == "none"; |
| 936 })) { |
| 937 return true; |
| 938 } |
| 939 |
| 940 // "While ancestor is not a block node and its parent is not null, set |
| 941 // ancestor to its parent." |
| 942 while (!isBlockNode(ancestor) |
| 943 && ancestor.parentNode) { |
| 944 ancestor = ancestor.parentNode; |
| 945 } |
| 946 |
| 947 // "Let reference be node." |
| 948 var reference = node; |
| 949 |
| 950 // "While reference is a descendant of ancestor:" |
| 951 while (reference != ancestor) { |
| 952 // "Let reference be the node before it in tree order." |
| 953 reference = previousNode(reference); |
| 954 |
| 955 // "If reference is a block node or a br, return true." |
| 956 if (isBlockNode(reference) |
| 957 || isHtmlElement(reference, "br")) { |
| 958 return true; |
| 959 } |
| 960 |
| 961 // "If reference is a Text node that is not a whitespace node, or is an |
| 962 // img, break from this loop." |
| 963 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference
)) |
| 964 || isHtmlElement(reference, "img")) { |
| 965 break; |
| 966 } |
| 967 } |
| 968 |
| 969 // "Let reference be node." |
| 970 reference = node; |
| 971 |
| 972 // "While reference is a descendant of ancestor:" |
| 973 var stop = nextNodeDescendants(ancestor); |
| 974 while (reference != stop) { |
| 975 // "Let reference be the node after it in tree order, or null if there |
| 976 // is no such node." |
| 977 reference = nextNode(reference); |
| 978 |
| 979 // "If reference is a block node or a br, return true." |
| 980 if (isBlockNode(reference) |
| 981 || isHtmlElement(reference, "br")) { |
| 982 return true; |
| 983 } |
| 984 |
| 985 // "If reference is a Text node that is not a whitespace node, or is an |
| 986 // img, break from this loop." |
| 987 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceN
ode(reference)) |
| 988 || isHtmlElement(reference, "img")) { |
| 989 break; |
| 990 } |
| 991 } |
| 992 |
| 993 // "Return false." |
| 994 return false; |
| 995 } |
| 996 |
| 997 // "Something is visible if it is a node that either is a block node, or a Text |
| 998 // node that is not a collapsed whitespace node, or an img, or a br that is not |
| 999 // an extraneous line break, or any node with a visible descendant; excluding |
| 1000 // any node with an ancestor container Element whose "display" property has |
| 1001 // resolved value "none"." |
| 1002 function isVisible(node) { |
| 1003 if (!node) { |
| 1004 return false; |
| 1005 } |
| 1006 |
| 1007 if (getAncestors(node).concat(node) |
| 1008 .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE }) |
| 1009 .some(function(node) { return getComputedStyle(node).display == "none" })) { |
| 1010 return false; |
| 1011 } |
| 1012 |
| 1013 if (isBlockNode(node) |
| 1014 || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) |
| 1015 || isHtmlElement(node, "img") |
| 1016 || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) { |
| 1017 return true; |
| 1018 } |
| 1019 |
| 1020 for (var i = 0; i < node.childNodes.length; i++) { |
| 1021 if (isVisible(node.childNodes[i])) { |
| 1022 return true; |
| 1023 } |
| 1024 } |
| 1025 |
| 1026 return false; |
| 1027 } |
| 1028 |
| 1029 // "Something is invisible if it is a node that is not visible." |
| 1030 function isInvisible(node) { |
| 1031 return node && !isVisible(node); |
| 1032 } |
| 1033 |
| 1034 // "A collapsed block prop is either a collapsed line break that is not an |
| 1035 // extraneous line break, or an Element that is an inline node and whose |
| 1036 // children are all either invisible or collapsed block props and that has at |
| 1037 // least one child that is a collapsed block prop." |
| 1038 function isCollapsedBlockProp(node) { |
| 1039 if (isCollapsedLineBreak(node) |
| 1040 && !isExtraneousLineBreak(node)) { |
| 1041 return true; |
| 1042 } |
| 1043 |
| 1044 if (!isInlineNode(node) |
| 1045 || node.nodeType != Node.ELEMENT_NODE) { |
| 1046 return false; |
| 1047 } |
| 1048 |
| 1049 var hasCollapsedBlockPropChild = false; |
| 1050 for (var i = 0; i < node.childNodes.length; i++) { |
| 1051 if (!isInvisible(node.childNodes[i]) |
| 1052 && !isCollapsedBlockProp(node.childNodes[i])) { |
| 1053 return false; |
| 1054 } |
| 1055 if (isCollapsedBlockProp(node.childNodes[i])) { |
| 1056 hasCollapsedBlockPropChild = true; |
| 1057 } |
| 1058 } |
| 1059 |
| 1060 return hasCollapsedBlockPropChild; |
| 1061 } |
| 1062 |
| 1063 // "The active range is the range of the selection given by calling |
| 1064 // getSelection() on the context object. (Thus the active range may be null.)" |
| 1065 // |
| 1066 // We cheat and return globalRange if that's defined. We also ensure that the |
| 1067 // active range meets the requirements that selection boundary points are |
| 1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that |
| 1069 // descend from a Document. |
| 1070 function getActiveRange() { |
| 1071 var ret; |
| 1072 if (globalRange) { |
| 1073 ret = globalRange; |
| 1074 } else if (getSelection().rangeCount) { |
| 1075 ret = getSelection().getRangeAt(0); |
| 1076 } else { |
| 1077 return null; |
| 1078 } |
| 1079 if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType)
== -1 |
| 1080 || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) ==
-1 |
| 1081 || !ret.startContainer.ownerDocument |
| 1082 || !ret.endContainer.ownerDocument |
| 1083 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) |
| 1084 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { |
| 1085 throw "Invalid active range; test bug?"; |
| 1086 } |
| 1087 return ret; |
| 1088 } |
| 1089 |
| 1090 // "For some commands, each HTMLDocument must have a boolean state override |
| 1091 // and/or a string value override. These do not change the command's state or |
| 1092 // value, but change the way some algorithms behave, as specified in those |
| 1093 // algorithms' definitions. Initially, both must be unset for every command. |
| 1094 // Whenever the number of ranges in the Selection changes to something |
| 1095 // different, and whenever a boundary point of the range at a given index in |
| 1096 // the Selection changes to something different, the state override and value |
| 1097 // override must be unset for every command." |
| 1098 // |
| 1099 // We implement this crudely by using setters and getters. To verify that the |
| 1100 // selection hasn't changed, we copy the active range and just check the |
| 1101 // endpoints match. This isn't really correct, but it's good enough for us. |
| 1102 // Unset state/value overrides are undefined. We put everything in a function |
| 1103 // so no one can access anything except via the provided functions, since |
| 1104 // otherwise callers might mistakenly use outdated overrides (if the selection |
| 1105 // has changed). |
| 1106 var getStateOverride, setStateOverride, unsetStateOverride, |
| 1107 getValueOverride, setValueOverride, unsetValueOverride; |
| 1108 (function() { |
| 1109 var stateOverrides = {}; |
| 1110 var valueOverrides = {}; |
| 1111 var storedRange = null; |
| 1112 |
| 1113 function resetOverrides() { |
| 1114 if (!storedRange |
| 1115 || storedRange.startContainer != getActiveRange().startContainer |
| 1116 || storedRange.endContainer != getActiveRange().endContainer |
| 1117 || storedRange.startOffset != getActiveRange().startOffset |
| 1118 || storedRange.endOffset != getActiveRange().endOffset) { |
| 1119 stateOverrides = {}; |
| 1120 valueOverrides = {}; |
| 1121 storedRange = getActiveRange().cloneRange(); |
| 1122 } |
| 1123 } |
| 1124 |
| 1125 getStateOverride = function(command) { |
| 1126 resetOverrides(); |
| 1127 return stateOverrides[command]; |
| 1128 }; |
| 1129 |
| 1130 setStateOverride = function(command, newState) { |
| 1131 resetOverrides(); |
| 1132 stateOverrides[command] = newState; |
| 1133 }; |
| 1134 |
| 1135 unsetStateOverride = function(command) { |
| 1136 resetOverrides(); |
| 1137 delete stateOverrides[command]; |
| 1138 } |
| 1139 |
| 1140 getValueOverride = function(command) { |
| 1141 resetOverrides(); |
| 1142 return valueOverrides[command]; |
| 1143 } |
| 1144 |
| 1145 // "The value override for the backColor command must be the same as the |
| 1146 // value override for the hiliteColor command, such that setting one sets |
| 1147 // the other to the same thing and unsetting one unsets the other." |
| 1148 setValueOverride = function(command, newValue) { |
| 1149 resetOverrides(); |
| 1150 valueOverrides[command] = newValue; |
| 1151 if (command == "backcolor") { |
| 1152 valueOverrides.hilitecolor = newValue; |
| 1153 } else if (command == "hilitecolor") { |
| 1154 valueOverrides.backcolor = newValue; |
| 1155 } |
| 1156 } |
| 1157 |
| 1158 unsetValueOverride = function(command) { |
| 1159 resetOverrides(); |
| 1160 delete valueOverrides[command]; |
| 1161 if (command == "backcolor") { |
| 1162 delete valueOverrides.hilitecolor; |
| 1163 } else if (command == "hilitecolor") { |
| 1164 delete valueOverrides.backcolor; |
| 1165 } |
| 1166 } |
| 1167 })(); |
| 1168 |
| 1169 //@} |
| 1170 |
| 1171 ///////////////////////////// |
| 1172 ///// Common algorithms ///// |
| 1173 ///////////////////////////// |
| 1174 |
| 1175 ///// Assorted common algorithms ///// |
| 1176 //@{ |
| 1177 |
| 1178 // Magic array of extra ranges whose endpoints we want to preserve. |
| 1179 var extraRanges = []; |
| 1180 |
| 1181 function movePreservingRanges(node, newParent, newIndex) { |
| 1182 // For convenience, I allow newIndex to be -1 to mean "insert at the end". |
| 1183 if (newIndex == -1) { |
| 1184 newIndex = newParent.childNodes.length; |
| 1185 } |
| 1186 |
| 1187 // "When the user agent is to move a Node to a new location, preserving |
| 1188 // ranges, it must remove the Node from its original parent (if any), then |
| 1189 // insert it in the new location. In doing so, however, it must ignore the |
| 1190 // regular range mutation rules, and instead follow these rules:" |
| 1191 |
| 1192 // "Let node be the moved Node, old parent and old index be the old parent |
| 1193 // (which may be null) and index, and new parent and new index be the new |
| 1194 // parent and index." |
| 1195 var oldParent = node.parentNode; |
| 1196 var oldIndex = getNodeIndex(node); |
| 1197 |
| 1198 // We preserve the global range object, the ranges in the selection, and |
| 1199 // any range that's in the extraRanges array. Any other ranges won't get |
| 1200 // updated, because we have no references to them. |
| 1201 var ranges = [globalRange].concat(extraRanges); |
| 1202 for (var i = 0; i < getSelection().rangeCount; i++) { |
| 1203 ranges.push(getSelection().getRangeAt(i)); |
| 1204 } |
| 1205 var boundaryPoints = []; |
| 1206 ranges.forEach(function(range) { |
| 1207 boundaryPoints.push([range.startContainer, range.startOffset]); |
| 1208 boundaryPoints.push([range.endContainer, range.endOffset]); |
| 1209 }); |
| 1210 |
| 1211 boundaryPoints.forEach(function(boundaryPoint) { |
| 1212 // "If a boundary point's node is the same as or a descendant of node, |
| 1213 // leave it unchanged, so it moves to the new location." |
| 1214 // |
| 1215 // No modifications necessary. |
| 1216 |
| 1217 // "If a boundary point's node is new parent and its offset is greater |
| 1218 // than new index, add one to its offset." |
| 1219 if (boundaryPoint[0] == newParent |
| 1220 && boundaryPoint[1] > newIndex) { |
| 1221 boundaryPoint[1]++; |
| 1222 } |
| 1223 |
| 1224 // "If a boundary point's node is old parent and its offset is old index
or |
| 1225 // old index + 1, set its node to new parent and add new index − old ind
ex |
| 1226 // to its offset." |
| 1227 if (boundaryPoint[0] == oldParent |
| 1228 && (boundaryPoint[1] == oldIndex |
| 1229 || boundaryPoint[1] == oldIndex + 1)) { |
| 1230 boundaryPoint[0] = newParent; |
| 1231 boundaryPoint[1] += newIndex - oldIndex; |
| 1232 } |
| 1233 |
| 1234 // "If a boundary point's node is old parent and its offset is greater t
han |
| 1235 // old index + 1, subtract one from its offset." |
| 1236 if (boundaryPoint[0] == oldParent |
| 1237 && boundaryPoint[1] > oldIndex + 1) { |
| 1238 boundaryPoint[1]--; |
| 1239 } |
| 1240 }); |
| 1241 |
| 1242 // Now actually move it and preserve the ranges. |
| 1243 if (newParent.childNodes.length == newIndex) { |
| 1244 newParent.appendChild(node); |
| 1245 } else { |
| 1246 newParent.insertBefore(node, newParent.childNodes[newIndex]); |
| 1247 } |
| 1248 |
| 1249 globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); |
| 1250 globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); |
| 1251 |
| 1252 for (var i = 0; i < extraRanges.length; i++) { |
| 1253 extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i +
2][1]); |
| 1254 extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3
][1]); |
| 1255 } |
| 1256 |
| 1257 getSelection().removeAllRanges(); |
| 1258 for (var i = 1 + extraRanges.length; i < ranges.length; i++) { |
| 1259 var newRange = document.createRange(); |
| 1260 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]); |
| 1261 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]); |
| 1262 getSelection().addRange(newRange); |
| 1263 } |
| 1264 } |
| 1265 |
| 1266 function setTagName(element, newName) { |
| 1267 // "If element is an HTML element with local name equal to new name, return |
| 1268 // element." |
| 1269 if (isHtmlElement(element, newName.toUpperCase())) { |
| 1270 return element; |
| 1271 } |
| 1272 |
| 1273 // "If element's parent is null, return element." |
| 1274 if (!element.parentNode) { |
| 1275 return element; |
| 1276 } |
| 1277 |
| 1278 // "Let replacement element be the result of calling createElement(new |
| 1279 // name) on the ownerDocument of element." |
| 1280 var replacementElement = element.ownerDocument.createElement(newName); |
| 1281 |
| 1282 // "Insert replacement element into element's parent immediately before |
| 1283 // element." |
| 1284 element.parentNode.insertBefore(replacementElement, element); |
| 1285 |
| 1286 // "Copy all attributes of element to replacement element, in order." |
| 1287 for (var i = 0; i < element.attributes.length; i++) { |
| 1288 replacementElement.setAttributeNS(element.attributes[i].namespaceURI, el
ement.attributes[i].name, element.attributes[i].value); |
| 1289 } |
| 1290 |
| 1291 // "While element has children, append the first child of element as the |
| 1292 // last child of replacement element, preserving ranges." |
| 1293 while (element.childNodes.length) { |
| 1294 movePreservingRanges(element.firstChild, replacementElement, replacement
Element.childNodes.length); |
| 1295 } |
| 1296 |
| 1297 // "Remove element from its parent." |
| 1298 element.parentNode.removeChild(element); |
| 1299 |
| 1300 // "Return replacement element." |
| 1301 return replacementElement; |
| 1302 } |
| 1303 |
| 1304 function removeExtraneousLineBreaksBefore(node) { |
| 1305 // "Let ref be the previousSibling of node." |
| 1306 var ref = node.previousSibling; |
| 1307 |
| 1308 // "If ref is null, abort these steps." |
| 1309 if (!ref) { |
| 1310 return; |
| 1311 } |
| 1312 |
| 1313 // "While ref has children, set ref to its lastChild." |
| 1314 while (ref.hasChildNodes()) { |
| 1315 ref = ref.lastChild; |
| 1316 } |
| 1317 |
| 1318 // "While ref is invisible but not an extraneous line break, and ref does |
| 1319 // not equal node's parent, set ref to the node before it in tree order." |
| 1320 while (isInvisible(ref) |
| 1321 && !isExtraneousLineBreak(ref) |
| 1322 && ref != node.parentNode) { |
| 1323 ref = previousNode(ref); |
| 1324 } |
| 1325 |
| 1326 // "If ref is an editable extraneous line break, remove it from its |
| 1327 // parent." |
| 1328 if (isEditable(ref) |
| 1329 && isExtraneousLineBreak(ref)) { |
| 1330 ref.parentNode.removeChild(ref); |
| 1331 } |
| 1332 } |
| 1333 |
| 1334 function removeExtraneousLineBreaksAtTheEndOf(node) { |
| 1335 // "Let ref be node." |
| 1336 var ref = node; |
| 1337 |
| 1338 // "While ref has children, set ref to its lastChild." |
| 1339 while (ref.hasChildNodes()) { |
| 1340 ref = ref.lastChild; |
| 1341 } |
| 1342 |
| 1343 // "While ref is invisible but not an extraneous line break, and ref does |
| 1344 // not equal node, set ref to the node before it in tree order." |
| 1345 while (isInvisible(ref) |
| 1346 && !isExtraneousLineBreak(ref) |
| 1347 && ref != node) { |
| 1348 ref = previousNode(ref); |
| 1349 } |
| 1350 |
| 1351 // "If ref is an editable extraneous line break:" |
| 1352 if (isEditable(ref) |
| 1353 && isExtraneousLineBreak(ref)) { |
| 1354 // "While ref's parent is editable and invisible, set ref to its |
| 1355 // parent." |
| 1356 while (isEditable(ref.parentNode) |
| 1357 && isInvisible(ref.parentNode)) { |
| 1358 ref = ref.parentNode; |
| 1359 } |
| 1360 |
| 1361 // "Remove ref from its parent." |
| 1362 ref.parentNode.removeChild(ref); |
| 1363 } |
| 1364 } |
| 1365 |
| 1366 // "To remove extraneous line breaks from a node, first remove extraneous line |
| 1367 // breaks before it, then remove extraneous line breaks at the end of it." |
| 1368 function removeExtraneousLineBreaksFrom(node) { |
| 1369 removeExtraneousLineBreaksBefore(node); |
| 1370 removeExtraneousLineBreaksAtTheEndOf(node); |
| 1371 } |
| 1372 |
| 1373 //@} |
| 1374 ///// Wrapping a list of nodes ///// |
| 1375 //@{ |
| 1376 |
| 1377 function wrap(nodeList, siblingCriteria, newParentInstructions) { |
| 1378 // "If not provided, sibling criteria returns false and new parent |
| 1379 // instructions returns null." |
| 1380 if (typeof siblingCriteria == "undefined") { |
| 1381 siblingCriteria = function() { return false }; |
| 1382 } |
| 1383 if (typeof newParentInstructions == "undefined") { |
| 1384 newParentInstructions = function() { return null }; |
| 1385 } |
| 1386 |
| 1387 // "If every member of node list is invisible, and none is a br, return |
| 1388 // null and abort these steps." |
| 1389 if (nodeList.every(isInvisible) |
| 1390 && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) { |
| 1391 return null; |
| 1392 } |
| 1393 |
| 1394 // "If node list's first member's parent is null, return null and abort |
| 1395 // these steps." |
| 1396 if (!nodeList[0].parentNode) { |
| 1397 return null; |
| 1398 } |
| 1399 |
| 1400 // "If node list's last member is an inline node that's not a br, and node |
| 1401 // list's last member's nextSibling is a br, append that br to node list." |
| 1402 if (isInlineNode(nodeList[nodeList.length - 1]) |
| 1403 && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
| 1404 && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { |
| 1405 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
| 1406 } |
| 1407 |
| 1408 // "While node list's first member's previousSibling is invisible, prepend |
| 1409 // it to node list." |
| 1410 while (isInvisible(nodeList[0].previousSibling)) { |
| 1411 nodeList.unshift(nodeList[0].previousSibling); |
| 1412 } |
| 1413 |
| 1414 // "While node list's last member's nextSibling is invisible, append it to |
| 1415 // node list." |
| 1416 while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) { |
| 1417 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
| 1418 } |
| 1419 |
| 1420 // "If the previousSibling of the first member of node list is editable and |
| 1421 // running sibling criteria on it returns true, let new parent be the |
| 1422 // previousSibling of the first member of node list." |
| 1423 var newParent; |
| 1424 if (isEditable(nodeList[0].previousSibling) |
| 1425 && siblingCriteria(nodeList[0].previousSibling)) { |
| 1426 newParent = nodeList[0].previousSibling; |
| 1427 |
| 1428 // "Otherwise, if the nextSibling of the last member of node list is |
| 1429 // editable and running sibling criteria on it returns true, let new parent |
| 1430 // be the nextSibling of the last member of node list." |
| 1431 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) |
| 1432 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { |
| 1433 newParent = nodeList[nodeList.length - 1].nextSibling; |
| 1434 |
| 1435 // "Otherwise, run new parent instructions, and let new parent be the |
| 1436 // result." |
| 1437 } else { |
| 1438 newParent = newParentInstructions(); |
| 1439 } |
| 1440 |
| 1441 // "If new parent is null, abort these steps and return null." |
| 1442 if (!newParent) { |
| 1443 return null; |
| 1444 } |
| 1445 |
| 1446 // "If new parent's parent is null:" |
| 1447 if (!newParent.parentNode) { |
| 1448 // "Insert new parent into the parent of the first member of node list |
| 1449 // immediately before the first member of node list." |
| 1450 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); |
| 1451 |
| 1452 // "If any range has a boundary point with node equal to the parent of |
| 1453 // new parent and offset equal to the index of new parent, add one to |
| 1454 // that boundary point's offset." |
| 1455 // |
| 1456 // Only try to fix the global range. |
| 1457 if (globalRange.startContainer == newParent.parentNode |
| 1458 && globalRange.startOffset == getNodeIndex(newParent)) { |
| 1459 globalRange.setStart(globalRange.startContainer, globalRange.startOf
fset + 1); |
| 1460 } |
| 1461 if (globalRange.endContainer == newParent.parentNode |
| 1462 && globalRange.endOffset == getNodeIndex(newParent)) { |
| 1463 globalRange.setEnd(globalRange.endContainer, globalRange.endOffset +
1); |
| 1464 } |
| 1465 } |
| 1466 |
| 1467 // "Let original parent be the parent of the first member of node list." |
| 1468 var originalParent = nodeList[0].parentNode; |
| 1469 |
| 1470 // "If new parent is before the first member of node list in tree order:" |
| 1471 if (isBefore(newParent, nodeList[0])) { |
| 1472 // "If new parent is not an inline node, but the last visible child of |
| 1473 // new parent and the first visible member of node list are both inline |
| 1474 // nodes, and the last child of new parent is not a br, call |
| 1475 // createElement("br") on the ownerDocument of new parent and append |
| 1476 // the result as the last child of new parent." |
| 1477 if (!isInlineNode(newParent) |
| 1478 && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1
)[0]) |
| 1479 && isInlineNode(nodeList.filter(isVisible)[0]) |
| 1480 && !isHtmlElement(newParent.lastChild, "BR")) { |
| 1481 newParent.appendChild(newParent.ownerDocument.createElement("br")); |
| 1482 } |
| 1483 |
| 1484 // "For each node in node list, append node as the last child of new |
| 1485 // parent, preserving ranges." |
| 1486 for (var i = 0; i < nodeList.length; i++) { |
| 1487 movePreservingRanges(nodeList[i], newParent, -1); |
| 1488 } |
| 1489 |
| 1490 // "Otherwise:" |
| 1491 } else { |
| 1492 // "If new parent is not an inline node, but the first visible child of |
| 1493 // new parent and the last visible member of node list are both inline |
| 1494 // nodes, and the last member of node list is not a br, call |
| 1495 // createElement("br") on the ownerDocument of new parent and insert |
| 1496 // the result as the first child of new parent." |
| 1497 if (!isInlineNode(newParent) |
| 1498 && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0]) |
| 1499 && isInlineNode(nodeList.filter(isVisible).slice(-1)[0]) |
| 1500 && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) { |
| 1501 newParent.insertBefore(newParent.ownerDocument.createElement("br"),
newParent.firstChild); |
| 1502 } |
| 1503 |
| 1504 // "For each node in node list, in reverse order, insert node as the |
| 1505 // first child of new parent, preserving ranges." |
| 1506 for (var i = nodeList.length - 1; i >= 0; i--) { |
| 1507 movePreservingRanges(nodeList[i], newParent, 0); |
| 1508 } |
| 1509 } |
| 1510 |
| 1511 // "If original parent is editable and has no children, remove it from its |
| 1512 // parent." |
| 1513 if (isEditable(originalParent) && !originalParent.hasChildNodes()) { |
| 1514 originalParent.parentNode.removeChild(originalParent); |
| 1515 } |
| 1516 |
| 1517 // "If new parent's nextSibling is editable and running sibling criteria on |
| 1518 // it returns true:" |
| 1519 if (isEditable(newParent.nextSibling) |
| 1520 && siblingCriteria(newParent.nextSibling)) { |
| 1521 // "If new parent is not an inline node, but new parent's last child |
| 1522 // and new parent's nextSibling's first child are both inline nodes, |
| 1523 // and new parent's last child is not a br, call createElement("br") on |
| 1524 // the ownerDocument of new parent and append the result as the last |
| 1525 // child of new parent." |
| 1526 if (!isInlineNode(newParent) |
| 1527 && isInlineNode(newParent.lastChild) |
| 1528 && isInlineNode(newParent.nextSibling.firstChild) |
| 1529 && !isHtmlElement(newParent.lastChild, "BR")) { |
| 1530 newParent.appendChild(newParent.ownerDocument.createElement("br")); |
| 1531 } |
| 1532 |
| 1533 // "While new parent's nextSibling has children, append its first child |
| 1534 // as the last child of new parent, preserving ranges." |
| 1535 while (newParent.nextSibling.hasChildNodes()) { |
| 1536 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1
); |
| 1537 } |
| 1538 |
| 1539 // "Remove new parent's nextSibling from its parent." |
| 1540 newParent.parentNode.removeChild(newParent.nextSibling); |
| 1541 } |
| 1542 |
| 1543 // "Remove extraneous line breaks from new parent." |
| 1544 removeExtraneousLineBreaksFrom(newParent); |
| 1545 |
| 1546 // "Return new parent." |
| 1547 return newParent; |
| 1548 } |
| 1549 |
| 1550 |
| 1551 //@} |
| 1552 ///// Allowed children ///// |
| 1553 //@{ |
| 1554 |
| 1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi", |
| 1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
| 1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
| 1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
| 1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." |
| 1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo", |
| 1561 "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
| 1562 "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
| 1563 "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
| 1564 "xmp", "big", "blink", "font", "marquee", "nobr", "tt"]; |
| 1565 |
| 1566 // "An element with inline contents is an HTML element whose local name is a |
| 1567 // name of an element with inline contents." |
| 1568 function isElementWithInlineContents(node) { |
| 1569 return isHtmlElement(node, namesOfElementsWithInlineContents); |
| 1570 } |
| 1571 |
| 1572 function isAllowedChild(child, parent_) { |
| 1573 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or |
| 1574 // an HTML element with local name equal to one of those, and child is a |
| 1575 // Text node whose data does not consist solely of space characters, return |
| 1576 // false." |
| 1577 if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_)
!= -1 |
| 1578 || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "
tr"])) |
| 1579 && typeof child == "object" |
| 1580 && child.nodeType == Node.TEXT_NODE |
| 1581 && !/^[ \t\n\f\r]*$/.test(child.data)) { |
| 1582 return false; |
| 1583 } |
| 1584 |
| 1585 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML |
| 1586 // element with local name equal to one of those, and child is not a Text |
| 1587 // node, return false." |
| 1588 if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1 |
| 1589 || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"])) |
| 1590 && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) { |
| 1591 return false; |
| 1592 } |
| 1593 |
| 1594 // "If child is a Document, DocumentFragment, or DocumentType, return |
| 1595 // false." |
| 1596 if (typeof child == "object" |
| 1597 && (child.nodeType == Node.DOCUMENT_NODE |
| 1598 || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE |
| 1599 || child.nodeType == Node.DOCUMENT_TYPE_NODE)) { |
| 1600 return false; |
| 1601 } |
| 1602 |
| 1603 // "If child is an HTML element, set child to the local name of child." |
| 1604 if (isHtmlElement(child)) { |
| 1605 child = child.tagName.toLowerCase(); |
| 1606 } |
| 1607 |
| 1608 // "If child is not a string, return true." |
| 1609 if (typeof child != "string") { |
| 1610 return true; |
| 1611 } |
| 1612 |
| 1613 // "If parent is an HTML element:" |
| 1614 if (isHtmlElement(parent_)) { |
| 1615 // "If child is "a", and parent or some ancestor of parent is an a, |
| 1616 // return false." |
| 1617 // |
| 1618 // "If child is a prohibited paragraph child name and parent or some |
| 1619 // ancestor of parent is an element with inline contents, return |
| 1620 // false." |
| 1621 // |
| 1622 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or |
| 1623 // some ancestor of parent is an HTML element with local name "h1", |
| 1624 // "h2", "h3", "h4", "h5", or "h6", return false." |
| 1625 var ancestor = parent_; |
| 1626 while (ancestor) { |
| 1627 if (child == "a" && isHtmlElement(ancestor, "a")) { |
| 1628 return false; |
| 1629 } |
| 1630 if (prohibitedParagraphChildNames.indexOf(child) != -1 |
| 1631 && isElementWithInlineContents(ancestor)) { |
| 1632 return false; |
| 1633 } |
| 1634 if (/^h[1-6]$/.test(child) |
| 1635 && isHtmlElement(ancestor) |
| 1636 && /^H[1-6]$/.test(ancestor.tagName)) { |
| 1637 return false; |
| 1638 } |
| 1639 ancestor = ancestor.parentNode; |
| 1640 } |
| 1641 |
| 1642 // "Let parent be the local name of parent." |
| 1643 parent_ = parent_.tagName.toLowerCase(); |
| 1644 } |
| 1645 |
| 1646 // "If parent is an Element or DocumentFragment, return true." |
| 1647 if (typeof parent_ == "object" |
| 1648 && (parent_.nodeType == Node.ELEMENT_NODE |
| 1649 || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { |
| 1650 return true; |
| 1651 } |
| 1652 |
| 1653 // "If parent is not a string, return false." |
| 1654 if (typeof parent_ != "string") { |
| 1655 return false; |
| 1656 } |
| 1657 |
| 1658 // "If parent is on the left-hand side of an entry on the following list, |
| 1659 // then return true if child is listed on the right-hand side of that |
| 1660 // entry, and false otherwise." |
| 1661 switch (parent_) { |
| 1662 case "colgroup": |
| 1663 return child == "col"; |
| 1664 case "table": |
| 1665 return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th",
"thead", "tr"].indexOf(child) != -1; |
| 1666 case "tbody": |
| 1667 case "thead": |
| 1668 case "tfoot": |
| 1669 return ["td", "th", "tr"].indexOf(child) != -1; |
| 1670 case "tr": |
| 1671 return ["td", "th"].indexOf(child) != -1; |
| 1672 case "dl": |
| 1673 return ["dt", "dd"].indexOf(child) != -1; |
| 1674 case "dir": |
| 1675 case "ol": |
| 1676 case "ul": |
| 1677 return ["dir", "li", "ol", "ul"].indexOf(child) != -1; |
| 1678 case "hgroup": |
| 1679 return /^h[1-6]$/.test(child); |
| 1680 } |
| 1681 |
| 1682 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", |
| 1683 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return |
| 1684 // false." |
| 1685 if (["body", "caption", "col", "colgroup", "frame", "frameset", "head", |
| 1686 "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) { |
| 1687 return false; |
| 1688 } |
| 1689 |
| 1690 // "If child is "dd" or "dt" and parent is not "dl", return false." |
| 1691 if (["dd", "dt"].indexOf(child) != -1 |
| 1692 && parent_ != "dl") { |
| 1693 return false; |
| 1694 } |
| 1695 |
| 1696 // "If child is "li" and parent is not "ol" or "ul", return false." |
| 1697 if (child == "li" |
| 1698 && parent_ != "ol" |
| 1699 && parent_ != "ul") { |
| 1700 return false; |
| 1701 } |
| 1702 |
| 1703 // "If parent is on the left-hand side of an entry on the following list |
| 1704 // and child is listed on the right-hand side of that entry, return false." |
| 1705 var table = [ |
| 1706 [["a"], ["a"]], |
| 1707 [["dd", "dt"], ["dd", "dt"]], |
| 1708 [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h
6"]], |
| 1709 [["li"], ["li"]], |
| 1710 [["nobr"], ["nobr"]], |
| 1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames], |
| 1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "t
h", "thead", "tr"]], |
| 1713 ]; |
| 1714 for (var i = 0; i < table.length; i++) { |
| 1715 if (table[i][0].indexOf(parent_) != -1 |
| 1716 && table[i][1].indexOf(child) != -1) { |
| 1717 return false; |
| 1718 } |
| 1719 } |
| 1720 |
| 1721 // "Return true." |
| 1722 return true; |
| 1723 } |
| 1724 |
| 1725 |
| 1726 //@} |
| 1727 |
| 1728 ////////////////////////////////////// |
| 1729 ///// Inline formatting commands ///// |
| 1730 ////////////////////////////////////// |
| 1731 |
| 1732 ///// Inline formatting command definitions ///// |
| 1733 //@{ |
| 1734 |
| 1735 // "A node node is effectively contained in a range range if range is not |
| 1736 // collapsed, and at least one of the following holds:" |
| 1737 function isEffectivelyContained(node, range) { |
| 1738 if (range.collapsed) { |
| 1739 return false; |
| 1740 } |
| 1741 |
| 1742 // "node is contained in range." |
| 1743 if (isContained(node, range)) { |
| 1744 return true; |
| 1745 } |
| 1746 |
| 1747 // "node is range's start node, it is a Text node, and its length is |
| 1748 // different from range's start offset." |
| 1749 if (node == range.startContainer |
| 1750 && node.nodeType == Node.TEXT_NODE |
| 1751 && getNodeLength(node) != range.startOffset) { |
| 1752 return true; |
| 1753 } |
| 1754 |
| 1755 // "node is range's end node, it is a Text node, and range's end offset is |
| 1756 // not 0." |
| 1757 if (node == range.endContainer |
| 1758 && node.nodeType == Node.TEXT_NODE |
| 1759 && range.endOffset != 0) { |
| 1760 return true; |
| 1761 } |
| 1762 |
| 1763 // "node has at least one child; and all its children are effectively |
| 1764 // contained in range; and either range's start node is not a descendant of |
| 1765 // node or is not a Text node or range's start offset is zero; and either |
| 1766 // range's end node is not a descendant of node or is not a Text node or |
| 1767 // range's end offset is its end node's length." |
| 1768 if (node.hasChildNodes() |
| 1769 && [].every.call(node.childNodes, function(child) { return isEffectivelyCont
ained(child, range) }) |
| 1770 && (!isDescendant(range.startContainer, node) |
| 1771 || range.startContainer.nodeType != Node.TEXT_NODE |
| 1772 || range.startOffset == 0) |
| 1773 && (!isDescendant(range.endContainer, node) |
| 1774 || range.endContainer.nodeType != Node.TEXT_NODE |
| 1775 || range.endOffset == getNodeLength(range.endContainer))) { |
| 1776 return true; |
| 1777 } |
| 1778 |
| 1779 return false; |
| 1780 } |
| 1781 |
| 1782 // Like get(All)ContainedNodes(), but for effectively contained nodes. |
| 1783 function getEffectivelyContainedNodes(range, condition) { |
| 1784 if (typeof condition == "undefined") { |
| 1785 condition = function() { return true }; |
| 1786 } |
| 1787 var node = range.startContainer; |
| 1788 while (isEffectivelyContained(node.parentNode, range)) { |
| 1789 node = node.parentNode; |
| 1790 } |
| 1791 |
| 1792 var stop = nextNodeDescendants(range.endContainer); |
| 1793 |
| 1794 var nodeList = []; |
| 1795 while (isBefore(node, stop)) { |
| 1796 if (isEffectivelyContained(node, range) |
| 1797 && condition(node)) { |
| 1798 nodeList.push(node); |
| 1799 node = nextNodeDescendants(node); |
| 1800 continue; |
| 1801 } |
| 1802 node = nextNode(node); |
| 1803 } |
| 1804 return nodeList; |
| 1805 } |
| 1806 |
| 1807 function getAllEffectivelyContainedNodes(range, condition) { |
| 1808 if (typeof condition == "undefined") { |
| 1809 condition = function() { return true }; |
| 1810 } |
| 1811 var node = range.startContainer; |
| 1812 while (isEffectivelyContained(node.parentNode, range)) { |
| 1813 node = node.parentNode; |
| 1814 } |
| 1815 |
| 1816 var stop = nextNodeDescendants(range.endContainer); |
| 1817 |
| 1818 var nodeList = []; |
| 1819 while (isBefore(node, stop)) { |
| 1820 if (isEffectivelyContained(node, range) |
| 1821 && condition(node)) { |
| 1822 nodeList.push(node); |
| 1823 } |
| 1824 node = nextNode(node); |
| 1825 } |
| 1826 return nodeList; |
| 1827 } |
| 1828 |
| 1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element |
| 1830 // with no attributes except possibly style; or a font element with no |
| 1831 // attributes except possibly style, color, face, and/or size; or an a element |
| 1832 // with no attributes except possibly style and/or href." |
| 1833 function isModifiableElement(node) { |
| 1834 if (!isHtmlElement(node)) { |
| 1835 return false; |
| 1836 } |
| 1837 |
| 1838 if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].ind
exOf(node.tagName) != -1) { |
| 1839 if (node.attributes.length == 0) { |
| 1840 return true; |
| 1841 } |
| 1842 |
| 1843 if (node.attributes.length == 1 |
| 1844 && node.hasAttribute("style")) { |
| 1845 return true; |
| 1846 } |
| 1847 } |
| 1848 |
| 1849 if (node.tagName == "FONT" || node.tagName == "A") { |
| 1850 var numAttrs = node.attributes.length; |
| 1851 |
| 1852 if (node.hasAttribute("style")) { |
| 1853 numAttrs--; |
| 1854 } |
| 1855 |
| 1856 if (node.tagName == "FONT") { |
| 1857 if (node.hasAttribute("color")) { |
| 1858 numAttrs--; |
| 1859 } |
| 1860 |
| 1861 if (node.hasAttribute("face")) { |
| 1862 numAttrs--; |
| 1863 } |
| 1864 |
| 1865 if (node.hasAttribute("size")) { |
| 1866 numAttrs--; |
| 1867 } |
| 1868 } |
| 1869 |
| 1870 if (node.tagName == "A" |
| 1871 && node.hasAttribute("href")) { |
| 1872 numAttrs--; |
| 1873 } |
| 1874 |
| 1875 if (numAttrs == 0) { |
| 1876 return true; |
| 1877 } |
| 1878 } |
| 1879 |
| 1880 return false; |
| 1881 } |
| 1882 |
| 1883 function isSimpleModifiableElement(node) { |
| 1884 // "A simple modifiable element is an HTML element for which at least one |
| 1885 // of the following holds:" |
| 1886 if (!isHtmlElement(node)) { |
| 1887 return false; |
| 1888 } |
| 1889 |
| 1890 // Only these elements can possibly be a simple modifiable element. |
| 1891 if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "S
UP", "U"].indexOf(node.tagName) == -1) { |
| 1892 return false; |
| 1893 } |
| 1894 |
| 1895 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
| 1896 // element with no attributes." |
| 1897 if (node.attributes.length == 0) { |
| 1898 return true; |
| 1899 } |
| 1900 |
| 1901 // If it's got more than one attribute, everything after this fails. |
| 1902 if (node.attributes.length > 1) { |
| 1903 return false; |
| 1904 } |
| 1905 |
| 1906 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
| 1907 // element with exactly one attribute, which is style, which sets no CSS |
| 1908 // properties (including invalid or unrecognized properties)." |
| 1909 // |
| 1910 // Not gonna try for invalid or unrecognized. |
| 1911 if (node.hasAttribute("style") |
| 1912 && node.style.length == 0) { |
| 1913 return true; |
| 1914 } |
| 1915 |
| 1916 // "It is an a element with exactly one attribute, which is href." |
| 1917 if (node.tagName == "A" |
| 1918 && node.hasAttribute("href")) { |
| 1919 return true; |
| 1920 } |
| 1921 |
| 1922 // "It is a font element with exactly one attribute, which is either color, |
| 1923 // face, or size." |
| 1924 if (node.tagName == "FONT" |
| 1925 && (node.hasAttribute("color") |
| 1926 || node.hasAttribute("face") |
| 1927 || node.hasAttribute("size") |
| 1928 )) { |
| 1929 return true; |
| 1930 } |
| 1931 |
| 1932 // "It is a b or strong element with exactly one attribute, which is style, |
| 1933 // and the style attribute sets exactly one CSS property (including invalid |
| 1934 // or unrecognized properties), which is "font-weight"." |
| 1935 if ((node.tagName == "B" || node.tagName == "STRONG") |
| 1936 && node.hasAttribute("style") |
| 1937 && node.style.length == 1 |
| 1938 && node.style.fontWeight != "") { |
| 1939 return true; |
| 1940 } |
| 1941 |
| 1942 // "It is an i or em element with exactly one attribute, which is style, |
| 1943 // and the style attribute sets exactly one CSS property (including invalid |
| 1944 // or unrecognized properties), which is "font-style"." |
| 1945 if ((node.tagName == "I" || node.tagName == "EM") |
| 1946 && node.hasAttribute("style") |
| 1947 && node.style.length == 1 |
| 1948 && node.style.fontStyle != "") { |
| 1949 return true; |
| 1950 } |
| 1951 |
| 1952 // "It is an a, font, or span element with exactly one attribute, which is |
| 1953 // style, and the style attribute sets exactly one CSS property (including |
| 1954 // invalid or unrecognized properties), and that property is not |
| 1955 // "text-decoration"." |
| 1956 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN"
) |
| 1957 && node.hasAttribute("style") |
| 1958 && node.style.length == 1 |
| 1959 && node.style.textDecoration == "") { |
| 1960 return true; |
| 1961 } |
| 1962 |
| 1963 // "It is an a, font, s, span, strike, or u element with exactly one |
| 1964 // attribute, which is style, and the style attribute sets exactly one CSS |
| 1965 // property (including invalid or unrecognized properties), which is |
| 1966 // "text-decoration", which is set to "line-through" or "underline" or |
| 1967 // "overline" or "none"." |
| 1968 // |
| 1969 // The weird extra node.style.length check is for Firefox, which as of |
| 1970 // 8.0a2 has annoying and weird behavior here. |
| 1971 if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1 |
| 1972 && node.hasAttribute("style") |
| 1973 && (node.style.length == 1 |
| 1974 || (node.style.length == 4 |
| 1975 && "MozTextBlink" in node.style |
| 1976 && "MozTextDecorationColor" in node.style |
| 1977 && "MozTextDecorationLine" in node.style |
| 1978 && "MozTextDecorationStyle" in node.style) |
| 1979 || (node.style.length == 4 |
| 1980 && "MozTextBlink" in node.style |
| 1981 && "textDecorationColor" in node.style |
| 1982 && "textDecorationLine" in node.style |
| 1983 && "textDecorationStyle" in node.style) |
| 1984 ) |
| 1985 && (node.style.textDecoration == "line-through" |
| 1986 || node.style.textDecoration == "underline" |
| 1987 || node.style.textDecoration == "overline" |
| 1988 || node.style.textDecoration == "none")) { |
| 1989 return true; |
| 1990 } |
| 1991 |
| 1992 return false; |
| 1993 } |
| 1994 |
| 1995 // "A formattable node is an editable visible node that is either a Text node, |
| 1996 // an img, or a br." |
| 1997 function isFormattableNode(node) { |
| 1998 return isEditable(node) |
| 1999 && isVisible(node) |
| 2000 && (node.nodeType == Node.TEXT_NODE |
| 2001 || isHtmlElement(node, ["img", "br"])); |
| 2002 } |
| 2003 |
| 2004 // "Two quantities are equivalent values for a command if either both are null, |
| 2005 // or both are strings and they're equal and the command does not define any |
| 2006 // equivalent values, or both are strings and the command defines equivalent |
| 2007 // values and they match the definition." |
| 2008 function areEquivalentValues(command, val1, val2) { |
| 2009 if (val1 === null && val2 === null) { |
| 2010 return true; |
| 2011 } |
| 2012 |
| 2013 if (typeof val1 == "string" |
| 2014 && typeof val2 == "string" |
| 2015 && val1 == val2 |
| 2016 && !("equivalentValues" in commands[command])) { |
| 2017 return true; |
| 2018 } |
| 2019 |
| 2020 if (typeof val1 == "string" |
| 2021 && typeof val2 == "string" |
| 2022 && "equivalentValues" in commands[command] |
| 2023 && commands[command].equivalentValues(val1, val2)) { |
| 2024 return true; |
| 2025 } |
| 2026 |
| 2027 return false; |
| 2028 } |
| 2029 |
| 2030 // "Two quantities are loosely equivalent values for a command if either they |
| 2031 // are equivalent values for the command, or if the command is the fontSize |
| 2032 // command; one of the quantities is one of "x-small", "small", "medium", |
| 2033 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is |
| 2034 // the resolved value of "font-size" on a font element whose size attribute has |
| 2035 // the corresponding value set ("1" through "7" respectively)." |
| 2036 function areLooselyEquivalentValues(command, val1, val2) { |
| 2037 if (areEquivalentValues(command, val1, val2)) { |
| 2038 return true; |
| 2039 } |
| 2040 |
| 2041 if (command != "fontsize" |
| 2042 || typeof val1 != "string" |
| 2043 || typeof val2 != "string") { |
| 2044 return false; |
| 2045 } |
| 2046 |
| 2047 // Static variables in JavaScript? |
| 2048 var callee = areLooselyEquivalentValues; |
| 2049 if (callee.sizeMap === undefined) { |
| 2050 callee.sizeMap = {}; |
| 2051 var font = document.createElement("font"); |
| 2052 document.body.appendChild(font); |
| 2053 ["x-small", "small", "medium", "large", "x-large", "xx-large", |
| 2054 "xxx-large"].forEach(function(keyword) { |
| 2055 font.size = cssSizeToLegacy(keyword); |
| 2056 callee.sizeMap[keyword] = getComputedStyle(font).fontSize; |
| 2057 }); |
| 2058 document.body.removeChild(font); |
| 2059 } |
| 2060 |
| 2061 return val1 === callee.sizeMap[val2] |
| 2062 || val2 === callee.sizeMap[val1]; |
| 2063 } |
| 2064 |
| 2065 //@} |
| 2066 ///// Assorted inline formatting command algorithms ///// |
| 2067 //@{ |
| 2068 |
| 2069 function getEffectiveCommandValue(node, command) { |
| 2070 // "If neither node nor its parent is an Element, return null." |
| 2071 if (node.nodeType != Node.ELEMENT_NODE |
| 2072 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) { |
| 2073 return null; |
| 2074 } |
| 2075 |
| 2076 // "If node is not an Element, return the effective command value of its |
| 2077 // parent for command." |
| 2078 if (node.nodeType != Node.ELEMENT_NODE) { |
| 2079 return getEffectiveCommandValue(node.parentNode, command); |
| 2080 } |
| 2081 |
| 2082 // "If command is "createLink" or "unlink":" |
| 2083 if (command == "createlink" || command == "unlink") { |
| 2084 // "While node is not null, and is not an a element that has an href |
| 2085 // attribute, set node to its parent." |
| 2086 while (node |
| 2087 && (!isHtmlElement(node) |
| 2088 || node.tagName != "A" |
| 2089 || !node.hasAttribute("href"))) { |
| 2090 node = node.parentNode; |
| 2091 } |
| 2092 |
| 2093 // "If node is null, return null." |
| 2094 if (!node) { |
| 2095 return null; |
| 2096 } |
| 2097 |
| 2098 // "Return the value of node's href attribute." |
| 2099 return node.getAttribute("href"); |
| 2100 } |
| 2101 |
| 2102 // "If command is "backColor" or "hiliteColor":" |
| 2103 if (command == "backcolor" |
| 2104 || command == "hilitecolor") { |
| 2105 // "While the resolved value of "background-color" on node is any |
| 2106 // fully transparent value, and node's parent is an Element, set |
| 2107 // node to its parent." |
| 2108 // |
| 2109 // Another lame hack to avoid flawed APIs. |
| 2110 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" |
| 2111 || getComputedStyle(node).backgroundColor === "" |
| 2112 || getComputedStyle(node).backgroundColor == "transparent") |
| 2113 && node.parentNode |
| 2114 && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
| 2115 node = node.parentNode; |
| 2116 } |
| 2117 |
| 2118 // "Return the resolved value of "background-color" for node." |
| 2119 return getComputedStyle(node).backgroundColor; |
| 2120 } |
| 2121 |
| 2122 // "If command is "subscript" or "superscript":" |
| 2123 if (command == "subscript" || command == "superscript") { |
| 2124 // "Let affected by subscript and affected by superscript be two |
| 2125 // boolean variables, both initially false." |
| 2126 var affectedBySubscript = false; |
| 2127 var affectedBySuperscript = false; |
| 2128 |
| 2129 // "While node is an inline node:" |
| 2130 while (isInlineNode(node)) { |
| 2131 var verticalAlign = getComputedStyle(node).verticalAlign; |
| 2132 |
| 2133 // "If node is a sub, set affected by subscript to true." |
| 2134 if (isHtmlElement(node, "sub")) { |
| 2135 affectedBySubscript = true; |
| 2136 // "Otherwise, if node is a sup, set affected by superscript to |
| 2137 // true." |
| 2138 } else if (isHtmlElement(node, "sup")) { |
| 2139 affectedBySuperscript = true; |
| 2140 } |
| 2141 |
| 2142 // "Set node to its parent." |
| 2143 node = node.parentNode; |
| 2144 } |
| 2145 |
| 2146 // "If affected by subscript and affected by superscript are both true, |
| 2147 // return the string "mixed"." |
| 2148 if (affectedBySubscript && affectedBySuperscript) { |
| 2149 return "mixed"; |
| 2150 } |
| 2151 |
| 2152 // "If affected by subscript is true, return "subscript"." |
| 2153 if (affectedBySubscript) { |
| 2154 return "subscript"; |
| 2155 } |
| 2156 |
| 2157 // "If affected by superscript is true, return "superscript"." |
| 2158 if (affectedBySuperscript) { |
| 2159 return "superscript"; |
| 2160 } |
| 2161 |
| 2162 // "Return null." |
| 2163 return null; |
| 2164 } |
| 2165 |
| 2166 // "If command is "strikethrough", and the "text-decoration" property of |
| 2167 // node or any of its ancestors has resolved value containing |
| 2168 // "line-through", return "line-through". Otherwise, return null." |
| 2169 if (command == "strikethrough") { |
| 2170 do { |
| 2171 if (getComputedStyle(node).textDecoration.indexOf("line-through") !=
-1) { |
| 2172 return "line-through"; |
| 2173 } |
| 2174 node = node.parentNode; |
| 2175 } while (node && node.nodeType == Node.ELEMENT_NODE); |
| 2176 return null; |
| 2177 } |
| 2178 |
| 2179 // "If command is "underline", and the "text-decoration" property of node |
| 2180 // or any of its ancestors has resolved value containing "underline", |
| 2181 // return "underline". Otherwise, return null." |
| 2182 if (command == "underline") { |
| 2183 do { |
| 2184 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1
) { |
| 2185 return "underline"; |
| 2186 } |
| 2187 node = node.parentNode; |
| 2188 } while (node && node.nodeType == Node.ELEMENT_NODE); |
| 2189 return null; |
| 2190 } |
| 2191 |
| 2192 if (!("relevantCssProperty" in commands[command])) { |
| 2193 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCo
mmandValue"; |
| 2194 } |
| 2195 |
| 2196 // "Return the resolved value for node of the relevant CSS property for |
| 2197 // command." |
| 2198 return getComputedStyle(node)[commands[command].relevantCssProperty]; |
| 2199 } |
| 2200 |
| 2201 function getSpecifiedCommandValue(element, command) { |
| 2202 // "If command is "backColor" or "hiliteColor" and element's display |
| 2203 // property does not have resolved value "inline", return null." |
| 2204 if ((command == "backcolor" || command == "hilitecolor") |
| 2205 && getComputedStyle(element).display != "inline") { |
| 2206 return null; |
| 2207 } |
| 2208 |
| 2209 // "If command is "createLink" or "unlink":" |
| 2210 if (command == "createlink" || command == "unlink") { |
| 2211 // "If element is an a element and has an href attribute, return the |
| 2212 // value of that attribute." |
| 2213 if (isHtmlElement(element) |
| 2214 && element.tagName == "A" |
| 2215 && element.hasAttribute("href")) { |
| 2216 return element.getAttribute("href"); |
| 2217 } |
| 2218 |
| 2219 // "Return null." |
| 2220 return null; |
| 2221 } |
| 2222 |
| 2223 // "If command is "subscript" or "superscript":" |
| 2224 if (command == "subscript" || command == "superscript") { |
| 2225 // "If element is a sup, return "superscript"." |
| 2226 if (isHtmlElement(element, "sup")) { |
| 2227 return "superscript"; |
| 2228 } |
| 2229 |
| 2230 // "If element is a sub, return "subscript"." |
| 2231 if (isHtmlElement(element, "sub")) { |
| 2232 return "subscript"; |
| 2233 } |
| 2234 |
| 2235 // "Return null." |
| 2236 return null; |
| 2237 } |
| 2238 |
| 2239 // "If command is "strikethrough", and element has a style attribute set, |
| 2240 // and that attribute sets "text-decoration":" |
| 2241 if (command == "strikethrough" |
| 2242 && element.style.textDecoration != "") { |
| 2243 // "If element's style attribute sets "text-decoration" to a value |
| 2244 // containing "line-through", return "line-through"." |
| 2245 if (element.style.textDecoration.indexOf("line-through") != -1) { |
| 2246 return "line-through"; |
| 2247 } |
| 2248 |
| 2249 // "Return null." |
| 2250 return null; |
| 2251 } |
| 2252 |
| 2253 // "If command is "strikethrough" and element is a s or strike element, |
| 2254 // return "line-through"." |
| 2255 if (command == "strikethrough" |
| 2256 && isHtmlElement(element, ["S", "STRIKE"])) { |
| 2257 return "line-through"; |
| 2258 } |
| 2259 |
| 2260 // "If command is "underline", and element has a style attribute set, and |
| 2261 // that attribute sets "text-decoration":" |
| 2262 if (command == "underline" |
| 2263 && element.style.textDecoration != "") { |
| 2264 // "If element's style attribute sets "text-decoration" to a value |
| 2265 // containing "underline", return "underline"." |
| 2266 if (element.style.textDecoration.indexOf("underline") != -1) { |
| 2267 return "underline"; |
| 2268 } |
| 2269 |
| 2270 // "Return null." |
| 2271 return null; |
| 2272 } |
| 2273 |
| 2274 // "If command is "underline" and element is a u element, return |
| 2275 // "underline"." |
| 2276 if (command == "underline" |
| 2277 && isHtmlElement(element, "U")) { |
| 2278 return "underline"; |
| 2279 } |
| 2280 |
| 2281 // "Let property be the relevant CSS property for command." |
| 2282 var property = commands[command].relevantCssProperty; |
| 2283 |
| 2284 // "If property is null, return null." |
| 2285 if (property === null) { |
| 2286 return null; |
| 2287 } |
| 2288 |
| 2289 // "If element has a style attribute set, and that attribute has the |
| 2290 // effect of setting property, return the value that it sets property to." |
| 2291 if (element.style[property] != "") { |
| 2292 return element.style[property]; |
| 2293 } |
| 2294 |
| 2295 // "If element is a font element that has an attribute whose effect is |
| 2296 // to create a presentational hint for property, return the value that the |
| 2297 // hint sets property to. (For a size of 7, this will be the non-CSS value |
| 2298 // "xxx-large".)" |
| 2299 if (isHtmlNamespace(element.namespaceURI) |
| 2300 && element.tagName == "FONT") { |
| 2301 if (property == "color" && element.hasAttribute("color")) { |
| 2302 return element.color; |
| 2303 } |
| 2304 if (property == "fontFamily" && element.hasAttribute("face")) { |
| 2305 return element.face; |
| 2306 } |
| 2307 if (property == "fontSize" && element.hasAttribute("size")) { |
| 2308 // This is not even close to correct in general. |
| 2309 var size = parseInt(element.size); |
| 2310 if (size < 1) { |
| 2311 size = 1; |
| 2312 } |
| 2313 if (size > 7) { |
| 2314 size = 7; |
| 2315 } |
| 2316 return { |
| 2317 1: "x-small", |
| 2318 2: "small", |
| 2319 3: "medium", |
| 2320 4: "large", |
| 2321 5: "x-large", |
| 2322 6: "xx-large", |
| 2323 7: "xxx-large" |
| 2324 }[size]; |
| 2325 } |
| 2326 } |
| 2327 |
| 2328 // "If element is in the following list, and property is equal to the |
| 2329 // CSS property name listed for it, return the string listed for it." |
| 2330 // |
| 2331 // A list follows, whose meaning is copied here. |
| 2332 if (property == "fontWeight" |
| 2333 && (element.tagName == "B" || element.tagName == "STRONG")) { |
| 2334 return "bold"; |
| 2335 } |
| 2336 if (property == "fontStyle" |
| 2337 && (element.tagName == "I" || element.tagName == "EM")) { |
| 2338 return "italic"; |
| 2339 } |
| 2340 |
| 2341 // "Return null." |
| 2342 return null; |
| 2343 } |
| 2344 |
| 2345 function reorderModifiableDescendants(node, command, newValue) { |
| 2346 // "Let candidate equal node." |
| 2347 var candidate = node; |
| 2348 |
| 2349 // "While candidate is a modifiable element, and candidate has exactly one |
| 2350 // child, and that child is also a modifiable element, and candidate is not |
| 2351 // a simple modifiable element or candidate's specified command value for |
| 2352 // command is not equivalent to new value, set candidate to its child." |
| 2353 while (isModifiableElement(candidate) |
| 2354 && candidate.childNodes.length == 1 |
| 2355 && isModifiableElement(candidate.firstChild) |
| 2356 && (!isSimpleModifiableElement(candidate) |
| 2357 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command
), newValue))) { |
| 2358 candidate = candidate.firstChild; |
| 2359 } |
| 2360 |
| 2361 // "If candidate is node, or is not a simple modifiable element, or its |
| 2362 // specified command value is not equivalent to new value, or its effective |
| 2363 // command value is not loosely equivalent to new value, abort these |
| 2364 // steps." |
| 2365 if (candidate == node |
| 2366 || !isSimpleModifiableElement(candidate) |
| 2367 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command
), newValue) |
| 2368 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate,
command), newValue)) { |
| 2369 return; |
| 2370 } |
| 2371 |
| 2372 // "While candidate has children, insert the first child of candidate into |
| 2373 // candidate's parent immediately before candidate, preserving ranges." |
| 2374 while (candidate.hasChildNodes()) { |
| 2375 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNode
Index(candidate)); |
| 2376 } |
| 2377 |
| 2378 // "Insert candidate into node's parent immediately after node." |
| 2379 node.parentNode.insertBefore(candidate, node.nextSibling); |
| 2380 |
| 2381 // "Append the node as the last child of candidate, preserving ranges." |
| 2382 movePreservingRanges(node, candidate, -1); |
| 2383 } |
| 2384 |
| 2385 function recordValues(nodeList) { |
| 2386 // "Let values be a list of (node, command, specified command value) |
| 2387 // triples, initially empty." |
| 2388 var values = []; |
| 2389 |
| 2390 // "For each node in node list, for each command in the list "subscript", |
| 2391 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", |
| 2392 // "strikethrough", and "underline" in that order:" |
| 2393 nodeList.forEach(function(node) { |
| 2394 ["subscript", "bold", "fontname", "fontsize", "forecolor", |
| 2395 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(
command) { |
| 2396 // "Let ancestor equal node." |
| 2397 var ancestor = node; |
| 2398 |
| 2399 // "If ancestor is not an Element, set it to its parent." |
| 2400 if (ancestor.nodeType != Node.ELEMENT_NODE) { |
| 2401 ancestor = ancestor.parentNode; |
| 2402 } |
| 2403 |
| 2404 // "While ancestor is an Element and its specified command value |
| 2405 // for command is null, set it to its parent." |
| 2406 while (ancestor |
| 2407 && ancestor.nodeType == Node.ELEMENT_NODE |
| 2408 && getSpecifiedCommandValue(ancestor, command) === null) { |
| 2409 ancestor = ancestor.parentNode; |
| 2410 } |
| 2411 |
| 2412 // "If ancestor is an Element, add (node, command, ancestor's |
| 2413 // specified command value for command) to values. Otherwise add |
| 2414 // (node, command, null) to values." |
| 2415 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) { |
| 2416 values.push([node, command, getSpecifiedCommandValue(ancestor, c
ommand)]); |
| 2417 } else { |
| 2418 values.push([node, command, null]); |
| 2419 } |
| 2420 }); |
| 2421 }); |
| 2422 |
| 2423 // "Return values." |
| 2424 return values; |
| 2425 } |
| 2426 |
| 2427 function restoreValues(values) { |
| 2428 // "For each (node, command, value) triple in values:" |
| 2429 values.forEach(function(triple) { |
| 2430 var node = triple[0]; |
| 2431 var command = triple[1]; |
| 2432 var value = triple[2]; |
| 2433 |
| 2434 // "Let ancestor equal node." |
| 2435 var ancestor = node; |
| 2436 |
| 2437 // "If ancestor is not an Element, set it to its parent." |
| 2438 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { |
| 2439 ancestor = ancestor.parentNode; |
| 2440 } |
| 2441 |
| 2442 // "While ancestor is an Element and its specified command value for |
| 2443 // command is null, set it to its parent." |
| 2444 while (ancestor |
| 2445 && ancestor.nodeType == Node.ELEMENT_NODE |
| 2446 && getSpecifiedCommandValue(ancestor, command) === null) { |
| 2447 ancestor = ancestor.parentNode; |
| 2448 } |
| 2449 |
| 2450 // "If value is null and ancestor is an Element, push down values on |
| 2451 // node for command, with new value null." |
| 2452 if (value === null |
| 2453 && ancestor |
| 2454 && ancestor.nodeType == Node.ELEMENT_NODE) { |
| 2455 pushDownValues(node, command, null); |
| 2456 |
| 2457 // "Otherwise, if ancestor is an Element and its specified command |
| 2458 // value for command is not equivalent to value, or if ancestor is not |
| 2459 // an Element and value is not null, force the value of command to |
| 2460 // value on node." |
| 2461 } else if ((ancestor |
| 2462 && ancestor.nodeType == Node.ELEMENT_NODE |
| 2463 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, comm
and), value)) |
| 2464 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) |
| 2465 && value !== null)) { |
| 2466 forceValue(node, command, value); |
| 2467 } |
| 2468 }); |
| 2469 } |
| 2470 |
| 2471 |
| 2472 //@} |
| 2473 ///// Clearing an element's value ///// |
| 2474 //@{ |
| 2475 |
| 2476 function clearValue(element, command) { |
| 2477 // "If element is not editable, return the empty list." |
| 2478 if (!isEditable(element)) { |
| 2479 return []; |
| 2480 } |
| 2481 |
| 2482 // "If element's specified command value for command is null, return the |
| 2483 // empty list." |
| 2484 if (getSpecifiedCommandValue(element, command) === null) { |
| 2485 return []; |
| 2486 } |
| 2487 |
| 2488 // "If element is a simple modifiable element:" |
| 2489 if (isSimpleModifiableElement(element)) { |
| 2490 // "Let children be the children of element." |
| 2491 var children = Array.prototype.slice.call(element.childNodes); |
| 2492 |
| 2493 // "For each child in children, insert child into element's parent |
| 2494 // immediately before element, preserving ranges." |
| 2495 for (var i = 0; i < children.length; i++) { |
| 2496 movePreservingRanges(children[i], element.parentNode, getNodeIndex(e
lement)); |
| 2497 } |
| 2498 |
| 2499 // "Remove element from its parent." |
| 2500 element.parentNode.removeChild(element); |
| 2501 |
| 2502 // "Return children." |
| 2503 return children; |
| 2504 } |
| 2505 |
| 2506 // "If command is "strikethrough", and element has a style attribute that |
| 2507 // sets "text-decoration" to some value containing "line-through", delete |
| 2508 // "line-through" from the value." |
| 2509 if (command == "strikethrough" |
| 2510 && element.style.textDecoration.indexOf("line-through") != -1) { |
| 2511 if (element.style.textDecoration == "line-through") { |
| 2512 element.style.textDecoration = ""; |
| 2513 } else { |
| 2514 element.style.textDecoration = element.style.textDecoration.replace(
"line-through", ""); |
| 2515 } |
| 2516 if (element.getAttribute("style") == "") { |
| 2517 element.removeAttribute("style"); |
| 2518 } |
| 2519 } |
| 2520 |
| 2521 // "If command is "underline", and element has a style attribute that sets |
| 2522 // "text-decoration" to some value containing "underline", delete |
| 2523 // "underline" from the value." |
| 2524 if (command == "underline" |
| 2525 && element.style.textDecoration.indexOf("underline") != -1) { |
| 2526 if (element.style.textDecoration == "underline") { |
| 2527 element.style.textDecoration = ""; |
| 2528 } else { |
| 2529 element.style.textDecoration = element.style.textDecoration.replace(
"underline", ""); |
| 2530 } |
| 2531 if (element.getAttribute("style") == "") { |
| 2532 element.removeAttribute("style"); |
| 2533 } |
| 2534 } |
| 2535 |
| 2536 // "If the relevant CSS property for command is not null, unset the CSS |
| 2537 // property property of element." |
| 2538 if (commands[command].relevantCssProperty !== null) { |
| 2539 element.style[commands[command].relevantCssProperty] = ''; |
| 2540 if (element.getAttribute("style") == "") { |
| 2541 element.removeAttribute("style"); |
| 2542 } |
| 2543 } |
| 2544 |
| 2545 // "If element is a font element:" |
| 2546 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { |
| 2547 // "If command is "foreColor", unset element's color attribute, if set." |
| 2548 if (command == "forecolor") { |
| 2549 element.removeAttribute("color"); |
| 2550 } |
| 2551 |
| 2552 // "If command is "fontName", unset element's face attribute, if set." |
| 2553 if (command == "fontname") { |
| 2554 element.removeAttribute("face"); |
| 2555 } |
| 2556 |
| 2557 // "If command is "fontSize", unset element's size attribute, if set." |
| 2558 if (command == "fontsize") { |
| 2559 element.removeAttribute("size"); |
| 2560 } |
| 2561 } |
| 2562 |
| 2563 // "If element is an a element and command is "createLink" or "unlink", |
| 2564 // unset the href property of element." |
| 2565 if (isHtmlElement(element, "A") |
| 2566 && (command == "createlink" || command == "unlink")) { |
| 2567 element.removeAttribute("href"); |
| 2568 } |
| 2569 |
| 2570 // "If element's specified command value for command is null, return the |
| 2571 // empty list." |
| 2572 if (getSpecifiedCommandValue(element, command) === null) { |
| 2573 return []; |
| 2574 } |
| 2575 |
| 2576 // "Set the tag name of element to "span", and return the one-node list |
| 2577 // consisting of the result." |
| 2578 return [setTagName(element, "span")]; |
| 2579 } |
| 2580 |
| 2581 |
| 2582 //@} |
| 2583 ///// Pushing down values ///// |
| 2584 //@{ |
| 2585 |
| 2586 function pushDownValues(node, command, newValue) { |
| 2587 // "If node's parent is not an Element, abort this algorithm." |
| 2588 if (!node.parentNode |
| 2589 || node.parentNode.nodeType != Node.ELEMENT_NODE) { |
| 2590 return; |
| 2591 } |
| 2592 |
| 2593 // "If the effective command value of command is loosely equivalent to new |
| 2594 // value on node, abort this algorithm." |
| 2595 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma
nd), newValue)) { |
| 2596 return; |
| 2597 } |
| 2598 |
| 2599 // "Let current ancestor be node's parent." |
| 2600 var currentAncestor = node.parentNode; |
| 2601 |
| 2602 // "Let ancestor list be a list of Nodes, initially empty." |
| 2603 var ancestorList = []; |
| 2604 |
| 2605 // "While current ancestor is an editable Element and the effective command |
| 2606 // value of command is not loosely equivalent to new value on it, append |
| 2607 // current ancestor to ancestor list, then set current ancestor to its |
| 2608 // parent." |
| 2609 while (isEditable(currentAncestor) |
| 2610 && currentAncestor.nodeType == Node.ELEMENT_NODE |
| 2611 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAnce
stor, command), newValue)) { |
| 2612 ancestorList.push(currentAncestor); |
| 2613 currentAncestor = currentAncestor.parentNode; |
| 2614 } |
| 2615 |
| 2616 // "If ancestor list is empty, abort this algorithm." |
| 2617 if (!ancestorList.length) { |
| 2618 return; |
| 2619 } |
| 2620 |
| 2621 // "Let propagated value be the specified command value of command on the |
| 2622 // last member of ancestor list." |
| 2623 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.len
gth - 1], command); |
| 2624 |
| 2625 // "If propagated value is null and is not equal to new value, abort this |
| 2626 // algorithm." |
| 2627 if (propagatedValue === null && propagatedValue != newValue) { |
| 2628 return; |
| 2629 } |
| 2630 |
| 2631 // "If the effective command value for the parent of the last member of |
| 2632 // ancestor list is not loosely equivalent to new value, and new value is |
| 2633 // not null, abort this algorithm." |
| 2634 if (newValue !== null |
| 2635 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorLis
t[ancestorList.length - 1].parentNode, command), newValue)) { |
| 2636 return; |
| 2637 } |
| 2638 |
| 2639 // "While ancestor list is not empty:" |
| 2640 while (ancestorList.length) { |
| 2641 // "Let current ancestor be the last member of ancestor list." |
| 2642 // "Remove the last member from ancestor list." |
| 2643 var currentAncestor = ancestorList.pop(); |
| 2644 |
| 2645 // "If the specified command value of current ancestor for command is |
| 2646 // not null, set propagated value to that value." |
| 2647 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
| 2648 propagatedValue = getSpecifiedCommandValue(currentAncestor, command)
; |
| 2649 } |
| 2650 |
| 2651 // "Let children be the children of current ancestor." |
| 2652 var children = Array.prototype.slice.call(currentAncestor.childNodes); |
| 2653 |
| 2654 // "If the specified command value of current ancestor for command is |
| 2655 // not null, clear the value of current ancestor." |
| 2656 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
| 2657 clearValue(currentAncestor, command); |
| 2658 } |
| 2659 |
| 2660 // "For every child in children:" |
| 2661 for (var i = 0; i < children.length; i++) { |
| 2662 var child = children[i]; |
| 2663 |
| 2664 // "If child is node, continue with the next child." |
| 2665 if (child == node) { |
| 2666 continue; |
| 2667 } |
| 2668 |
| 2669 // "If child is an Element whose specified command value for |
| 2670 // command is neither null nor equivalent to propagated value, |
| 2671 // continue with the next child." |
| 2672 if (child.nodeType == Node.ELEMENT_NODE |
| 2673 && getSpecifiedCommandValue(child, command) !== null |
| 2674 && !areEquivalentValues(command, propagatedValue, getSpecifiedComman
dValue(child, command))) { |
| 2675 continue; |
| 2676 } |
| 2677 |
| 2678 // "If child is the last member of ancestor list, continue with the |
| 2679 // next child." |
| 2680 if (child == ancestorList[ancestorList.length - 1]) { |
| 2681 continue; |
| 2682 } |
| 2683 |
| 2684 // "Force the value of child, with command as in this algorithm |
| 2685 // and new value equal to propagated value." |
| 2686 forceValue(child, command, propagatedValue); |
| 2687 } |
| 2688 } |
| 2689 } |
| 2690 |
| 2691 |
| 2692 //@} |
| 2693 ///// Forcing the value of a node ///// |
| 2694 //@{ |
| 2695 |
| 2696 function forceValue(node, command, newValue) { |
| 2697 // "If node's parent is null, abort this algorithm." |
| 2698 if (!node.parentNode) { |
| 2699 return; |
| 2700 } |
| 2701 |
| 2702 // "If new value is null, abort this algorithm." |
| 2703 if (newValue === null) { |
| 2704 return; |
| 2705 } |
| 2706 |
| 2707 // "If node is an allowed child of "span":" |
| 2708 if (isAllowedChild(node, "span")) { |
| 2709 // "Reorder modifiable descendants of node's previousSibling." |
| 2710 reorderModifiableDescendants(node.previousSibling, command, newValue); |
| 2711 |
| 2712 // "Reorder modifiable descendants of node's nextSibling." |
| 2713 reorderModifiableDescendants(node.nextSibling, command, newValue); |
| 2714 |
| 2715 // "Wrap the one-node list consisting of node, with sibling criteria |
| 2716 // returning true for a simple modifiable element whose specified |
| 2717 // command value is equivalent to new value and whose effective command |
| 2718 // value is loosely equivalent to new value and false otherwise, and |
| 2719 // with new parent instructions returning null." |
| 2720 wrap([node], |
| 2721 function(node) { |
| 2722 return isSimpleModifiableElement(node) |
| 2723 && areEquivalentValues(command, getSpecifiedCommandValue(nod
e, command), newValue) |
| 2724 && areLooselyEquivalentValues(command, getEffectiveCommandVa
lue(node, command), newValue); |
| 2725 }, |
| 2726 function() { return null } |
| 2727 ); |
| 2728 } |
| 2729 |
| 2730 // "If node is invisible, abort this algorithm." |
| 2731 if (isInvisible(node)) { |
| 2732 return; |
| 2733 } |
| 2734 |
| 2735 // "If the effective command value of command is loosely equivalent to new |
| 2736 // value on node, abort this algorithm." |
| 2737 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma
nd), newValue)) { |
| 2738 return; |
| 2739 } |
| 2740 |
| 2741 // "If node is not an allowed child of "span":" |
| 2742 if (!isAllowedChild(node, "span")) { |
| 2743 // "Let children be all children of node, omitting any that are |
| 2744 // Elements whose specified command value for command is neither null |
| 2745 // nor equivalent to new value." |
| 2746 var children = []; |
| 2747 for (var i = 0; i < node.childNodes.length; i++) { |
| 2748 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
| 2749 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i]
, command); |
| 2750 |
| 2751 if (specifiedValue !== null |
| 2752 && !areEquivalentValues(command, newValue, specifiedValue)) { |
| 2753 continue; |
| 2754 } |
| 2755 } |
| 2756 children.push(node.childNodes[i]); |
| 2757 } |
| 2758 |
| 2759 // "Force the value of each Node in children, with command and new |
| 2760 // value as in this invocation of the algorithm." |
| 2761 for (var i = 0; i < children.length; i++) { |
| 2762 forceValue(children[i], command, newValue); |
| 2763 } |
| 2764 |
| 2765 // "Abort this algorithm." |
| 2766 return; |
| 2767 } |
| 2768 |
| 2769 // "If the effective command value of command is loosely equivalent to new |
| 2770 // value on node, abort this algorithm." |
| 2771 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma
nd), newValue)) { |
| 2772 return; |
| 2773 } |
| 2774 |
| 2775 // "Let new parent be null." |
| 2776 var newParent = null; |
| 2777 |
| 2778 // "If the CSS styling flag is false:" |
| 2779 if (!cssStylingFlag) { |
| 2780 // "If command is "bold" and new value is "bold", let new parent be the |
| 2781 // result of calling createElement("b") on the ownerDocument of node." |
| 2782 if (command == "bold" && (newValue == "bold" || newValue == "700")) { |
| 2783 newParent = node.ownerDocument.createElement("b"); |
| 2784 } |
| 2785 |
| 2786 // "If command is "italic" and new value is "italic", let new parent be |
| 2787 // the result of calling createElement("i") on the ownerDocument of |
| 2788 // node." |
| 2789 if (command == "italic" && newValue == "italic") { |
| 2790 newParent = node.ownerDocument.createElement("i"); |
| 2791 } |
| 2792 |
| 2793 // "If command is "strikethrough" and new value is "line-through", let |
| 2794 // new parent be the result of calling createElement("s") on the |
| 2795 // ownerDocument of node." |
| 2796 if (command == "strikethrough" && newValue == "line-through") { |
| 2797 newParent = node.ownerDocument.createElement("s"); |
| 2798 } |
| 2799 |
| 2800 // "If command is "underline" and new value is "underline", let new |
| 2801 // parent be the result of calling createElement("u") on the |
| 2802 // ownerDocument of node." |
| 2803 if (command == "underline" && newValue == "underline") { |
| 2804 newParent = node.ownerDocument.createElement("u"); |
| 2805 } |
| 2806 |
| 2807 // "If command is "foreColor", and new value is fully opaque with red, |
| 2808 // green, and blue components in the range 0 to 255:" |
| 2809 if (command == "forecolor" && parseSimpleColor(newValue)) { |
| 2810 // "Let new parent be the result of calling createElement("font") |
| 2811 // on the ownerDocument of node." |
| 2812 newParent = node.ownerDocument.createElement("font"); |
| 2813 |
| 2814 // "Set the color attribute of new parent to the result of applying |
| 2815 // the rules for serializing simple color values to new value |
| 2816 // (interpreted as a simple color)." |
| 2817 newParent.setAttribute("color", parseSimpleColor(newValue)); |
| 2818 } |
| 2819 |
| 2820 // "If command is "fontName", let new parent be the result of calling |
| 2821 // createElement("font") on the ownerDocument of node, then set the |
| 2822 // face attribute of new parent to new value." |
| 2823 if (command == "fontname") { |
| 2824 newParent = node.ownerDocument.createElement("font"); |
| 2825 newParent.face = newValue; |
| 2826 } |
| 2827 } |
| 2828 |
| 2829 // "If command is "createLink" or "unlink":" |
| 2830 if (command == "createlink" || command == "unlink") { |
| 2831 // "Let new parent be the result of calling createElement("a") on the |
| 2832 // ownerDocument of node." |
| 2833 newParent = node.ownerDocument.createElement("a"); |
| 2834 |
| 2835 // "Set the href attribute of new parent to new value." |
| 2836 newParent.setAttribute("href", newValue); |
| 2837 |
| 2838 // "Let ancestor be node's parent." |
| 2839 var ancestor = node.parentNode; |
| 2840 |
| 2841 // "While ancestor is not null:" |
| 2842 while (ancestor) { |
| 2843 // "If ancestor is an a, set the tag name of ancestor to "span", |
| 2844 // and let ancestor be the result." |
| 2845 if (isHtmlElement(ancestor, "A")) { |
| 2846 ancestor = setTagName(ancestor, "span"); |
| 2847 } |
| 2848 |
| 2849 // "Set ancestor to its parent." |
| 2850 ancestor = ancestor.parentNode; |
| 2851 } |
| 2852 } |
| 2853 |
| 2854 // "If command is "fontSize"; and new value is one of "x-small", "small", |
| 2855 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the |
| 2856 // CSS styling flag is false, or new value is "xxx-large": let new parent |
| 2857 // be the result of calling createElement("font") on the ownerDocument of |
| 2858 // node, then set the size attribute of new parent to the number from the |
| 2859 // following table based on new value: [table omitted]" |
| 2860 if (command == "fontsize" |
| 2861 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large
"].indexOf(newValue) != -1 |
| 2862 && (!cssStylingFlag || newValue == "xxx-large")) { |
| 2863 newParent = node.ownerDocument.createElement("font"); |
| 2864 newParent.size = cssSizeToLegacy(newValue); |
| 2865 } |
| 2866 |
| 2867 // "If command is "subscript" or "superscript" and new value is |
| 2868 // "subscript", let new parent be the result of calling |
| 2869 // createElement("sub") on the ownerDocument of node." |
| 2870 if ((command == "subscript" || command == "superscript") |
| 2871 && newValue == "subscript") { |
| 2872 newParent = node.ownerDocument.createElement("sub"); |
| 2873 } |
| 2874 |
| 2875 // "If command is "subscript" or "superscript" and new value is |
| 2876 // "superscript", let new parent be the result of calling |
| 2877 // createElement("sup") on the ownerDocument of node." |
| 2878 if ((command == "subscript" || command == "superscript") |
| 2879 && newValue == "superscript") { |
| 2880 newParent = node.ownerDocument.createElement("sup"); |
| 2881 } |
| 2882 |
| 2883 // "If new parent is null, let new parent be the result of calling |
| 2884 // createElement("span") on the ownerDocument of node." |
| 2885 if (!newParent) { |
| 2886 newParent = node.ownerDocument.createElement("span"); |
| 2887 } |
| 2888 |
| 2889 // "Insert new parent in node's parent before node." |
| 2890 node.parentNode.insertBefore(newParent, node); |
| 2891 |
| 2892 // "If the effective command value of command for new parent is not loosely |
| 2893 // equivalent to new value, and the relevant CSS property for command is |
| 2894 // not null, set that CSS property of new parent to new value (if the new |
| 2895 // value would be valid)." |
| 2896 var property = commands[command].relevantCssProperty; |
| 2897 if (property !== null |
| 2898 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent,
command), newValue)) { |
| 2899 newParent.style[property] = newValue; |
| 2900 } |
| 2901 |
| 2902 // "If command is "strikethrough", and new value is "line-through", and the |
| 2903 // effective command value of "strikethrough" for new parent is not |
| 2904 // "line-through", set the "text-decoration" property of new parent to |
| 2905 // "line-through"." |
| 2906 if (command == "strikethrough" |
| 2907 && newValue == "line-through" |
| 2908 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { |
| 2909 newParent.style.textDecoration = "line-through"; |
| 2910 } |
| 2911 |
| 2912 // "If command is "underline", and new value is "underline", and the |
| 2913 // effective command value of "underline" for new parent is not |
| 2914 // "underline", set the "text-decoration" property of new parent to |
| 2915 // "underline"." |
| 2916 if (command == "underline" |
| 2917 && newValue == "underline" |
| 2918 && getEffectiveCommandValue(newParent, "underline") != "underline") { |
| 2919 newParent.style.textDecoration = "underline"; |
| 2920 } |
| 2921 |
| 2922 // "Append node to new parent as its last child, preserving ranges." |
| 2923 movePreservingRanges(node, newParent, newParent.childNodes.length); |
| 2924 |
| 2925 // "If node is an Element and the effective command value of command for |
| 2926 // node is not loosely equivalent to new value:" |
| 2927 if (node.nodeType == Node.ELEMENT_NODE |
| 2928 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), ne
wValue)) { |
| 2929 // "Insert node into the parent of new parent before new parent, |
| 2930 // preserving ranges." |
| 2931 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent)
); |
| 2932 |
| 2933 // "Remove new parent from its parent." |
| 2934 newParent.parentNode.removeChild(newParent); |
| 2935 |
| 2936 // "Let children be all children of node, omitting any that are |
| 2937 // Elements whose specified command value for command is neither null |
| 2938 // nor equivalent to new value." |
| 2939 var children = []; |
| 2940 for (var i = 0; i < node.childNodes.length; i++) { |
| 2941 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
| 2942 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i]
, command); |
| 2943 |
| 2944 if (specifiedValue !== null |
| 2945 && !areEquivalentValues(command, newValue, specifiedValue)) { |
| 2946 continue; |
| 2947 } |
| 2948 } |
| 2949 children.push(node.childNodes[i]); |
| 2950 } |
| 2951 |
| 2952 // "Force the value of each Node in children, with command and new |
| 2953 // value as in this invocation of the algorithm." |
| 2954 for (var i = 0; i < children.length; i++) { |
| 2955 forceValue(children[i], command, newValue); |
| 2956 } |
| 2957 } |
| 2958 } |
| 2959 |
| 2960 |
| 2961 //@} |
| 2962 ///// Setting the selection's value ///// |
| 2963 //@{ |
| 2964 |
| 2965 function setSelectionValue(command, newValue) { |
| 2966 // "If there is no formattable node effectively contained in the active |
| 2967 // range:" |
| 2968 if (!getAllEffectivelyContainedNodes(getActiveRange()) |
| 2969 .some(isFormattableNode)) { |
| 2970 // "If command has inline command activated values, set the state |
| 2971 // override to true if new value is among them and false if it's not." |
| 2972 if ("inlineCommandActivatedValues" in commands[command]) { |
| 2973 setStateOverride(command, commands[command].inlineCommandActivatedVa
lues |
| 2974 .indexOf(newValue) != -1); |
| 2975 } |
| 2976 |
| 2977 // "If command is "subscript", unset the state override for |
| 2978 // "superscript"." |
| 2979 if (command == "subscript") { |
| 2980 unsetStateOverride("superscript"); |
| 2981 } |
| 2982 |
| 2983 // "If command is "superscript", unset the state override for |
| 2984 // "subscript"." |
| 2985 if (command == "superscript") { |
| 2986 unsetStateOverride("subscript"); |
| 2987 } |
| 2988 |
| 2989 // "If new value is null, unset the value override (if any)." |
| 2990 if (newValue === null) { |
| 2991 unsetValueOverride(command); |
| 2992 |
| 2993 // "Otherwise, if command is "createLink" or it has a value specified, |
| 2994 // set the value override to new value." |
| 2995 } else if (command == "createlink" || "value" in commands[command]) { |
| 2996 setValueOverride(command, newValue); |
| 2997 } |
| 2998 |
| 2999 // "Abort these steps." |
| 3000 return; |
| 3001 } |
| 3002 |
| 3003 // "If the active range's start node is an editable Text node, and its |
| 3004 // start offset is neither zero nor its start node's length, call |
| 3005 // splitText() on the active range's start node, with argument equal to the |
| 3006 // active range's start offset. Then set the active range's start node to |
| 3007 // the result, and its start offset to zero." |
| 3008 if (isEditable(getActiveRange().startContainer) |
| 3009 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 3010 && getActiveRange().startOffset != 0 |
| 3011 && getActiveRange().startOffset != getNodeLength(getActiveRange().startConta
iner)) { |
| 3012 // Account for browsers not following range mutation rules |
| 3013 var newActiveRange = document.createRange(); |
| 3014 var newNode; |
| 3015 if (getActiveRange().startContainer == getActiveRange().endContainer) { |
| 3016 var newEndOffset = getActiveRange().endOffset - getActiveRange().sta
rtOffset; |
| 3017 newNode = getActiveRange().startContainer.splitText(getActiveRange()
.startOffset); |
| 3018 newActiveRange.setEnd(newNode, newEndOffset); |
| 3019 getActiveRange().setEnd(newNode, newEndOffset); |
| 3020 } else { |
| 3021 newNode = getActiveRange().startContainer.splitText(getActiveRange()
.startOffset); |
| 3022 } |
| 3023 newActiveRange.setStart(newNode, 0); |
| 3024 getSelection().removeAllRanges(); |
| 3025 getSelection().addRange(newActiveRange); |
| 3026 |
| 3027 getActiveRange().setStart(newNode, 0); |
| 3028 } |
| 3029 |
| 3030 // "If the active range's end node is an editable Text node, and its end |
| 3031 // offset is neither zero nor its end node's length, call splitText() on |
| 3032 // the active range's end node, with argument equal to the active range's |
| 3033 // end offset." |
| 3034 if (isEditable(getActiveRange().endContainer) |
| 3035 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
| 3036 && getActiveRange().endOffset != 0 |
| 3037 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer
)) { |
| 3038 // IE seems to mutate the range incorrectly here, so we need correction |
| 3039 // here as well. The active range will be temporarily in orphaned |
| 3040 // nodes, so calling getActiveRange() after splitText() but before |
| 3041 // fixing the range will throw an exception. |
| 3042 var activeRange = getActiveRange(); |
| 3043 var newStart = [activeRange.startContainer, activeRange.startOffset]; |
| 3044 var newEnd = [activeRange.endContainer, activeRange.endOffset]; |
| 3045 activeRange.endContainer.splitText(activeRange.endOffset); |
| 3046 activeRange.setStart(newStart[0], newStart[1]); |
| 3047 activeRange.setEnd(newEnd[0], newEnd[1]); |
| 3048 |
| 3049 getSelection().removeAllRanges(); |
| 3050 getSelection().addRange(activeRange); |
| 3051 } |
| 3052 |
| 3053 // "Let element list be all editable Elements effectively contained in the |
| 3054 // active range. |
| 3055 // |
| 3056 // "For each element in element list, clear the value of element." |
| 3057 getAllEffectivelyContainedNodes(getActiveRange(), function(node) { |
| 3058 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE; |
| 3059 }).forEach(function(element) { |
| 3060 clearValue(element, command); |
| 3061 }); |
| 3062 |
| 3063 // "Let node list be all editable nodes effectively contained in the active |
| 3064 // range. |
| 3065 // |
| 3066 // "For each node in node list:" |
| 3067 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(functi
on(node) { |
| 3068 // "Push down values on node." |
| 3069 pushDownValues(node, command, newValue); |
| 3070 |
| 3071 // "If node is an allowed child of span, force the value of node." |
| 3072 if (isAllowedChild(node, "span")) { |
| 3073 forceValue(node, command, newValue); |
| 3074 } |
| 3075 }); |
| 3076 } |
| 3077 |
| 3078 |
| 3079 //@} |
| 3080 ///// The backColor command ///// |
| 3081 //@{ |
| 3082 commands.backcolor = { |
| 3083 // Copy-pasted, same as hiliteColor |
| 3084 action: function(value) { |
| 3085 // Action is further copy-pasted, same as foreColor |
| 3086 |
| 3087 // "If value is not a valid CSS color, prepend "#" to it." |
| 3088 // |
| 3089 // "If value is still not a valid CSS color, or if it is currentColor, |
| 3090 // return false." |
| 3091 // |
| 3092 // Cheap hack for testing, no attempt to be comprehensive. |
| 3093 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
| 3094 value = "#" + value; |
| 3095 } |
| 3096 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
| 3097 && !parseSimpleColor(value) |
| 3098 && value.toLowerCase() != "transparent") { |
| 3099 return false; |
| 3100 } |
| 3101 |
| 3102 // "Set the selection's value to value." |
| 3103 setSelectionValue("backcolor", value); |
| 3104 |
| 3105 // "Return true." |
| 3106 return true; |
| 3107 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
| 3108 equivalentValues: function(val1, val2) { |
| 3109 // "Either both strings are valid CSS colors and have the same red, |
| 3110 // green, blue, and alpha components, or neither string is a valid CSS |
| 3111 // color." |
| 3112 return normalizeColor(val1) === normalizeColor(val2); |
| 3113 }, |
| 3114 }; |
| 3115 |
| 3116 //@} |
| 3117 ///// The bold command ///// |
| 3118 //@{ |
| 3119 commands.bold = { |
| 3120 action: function() { |
| 3121 // "If queryCommandState("bold") returns true, set the selection's |
| 3122 // value to "normal". Otherwise set the selection's value to "bold". |
| 3123 // Either way, return true." |
| 3124 if (myQueryCommandState("bold")) { |
| 3125 setSelectionValue("bold", "normal"); |
| 3126 } else { |
| 3127 setSelectionValue("bold", "bold"); |
| 3128 } |
| 3129 return true; |
| 3130 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], |
| 3131 relevantCssProperty: "fontWeight", |
| 3132 equivalentValues: function(val1, val2) { |
| 3133 // "Either the two strings are equal, or one is "bold" and the other is |
| 3134 // "700", or one is "normal" and the other is "400"." |
| 3135 return val1 == val2 |
| 3136 || (val1 == "bold" && val2 == "700") |
| 3137 || (val1 == "700" && val2 == "bold") |
| 3138 || (val1 == "normal" && val2 == "400") |
| 3139 || (val1 == "400" && val2 == "normal"); |
| 3140 }, |
| 3141 }; |
| 3142 |
| 3143 //@} |
| 3144 ///// The createLink command ///// |
| 3145 //@{ |
| 3146 commands.createlink = { |
| 3147 action: function(value) { |
| 3148 // "If value is the empty string, return false." |
| 3149 if (value === "") { |
| 3150 return false; |
| 3151 } |
| 3152 |
| 3153 // "For each editable a element that has an href attribute and is an |
| 3154 // ancestor of some node effectively contained in the active range, set |
| 3155 // that a element's href attribute to value." |
| 3156 // |
| 3157 // TODO: We don't actually do this in tree order, not that it matters |
| 3158 // unless you're spying with mutation events. |
| 3159 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node)
{ |
| 3160 getAncestors(node).forEach(function(ancestor) { |
| 3161 if (isEditable(ancestor) |
| 3162 && isHtmlElement(ancestor, "a") |
| 3163 && ancestor.hasAttribute("href")) { |
| 3164 ancestor.setAttribute("href", value); |
| 3165 } |
| 3166 }); |
| 3167 }); |
| 3168 |
| 3169 // "Set the selection's value to value." |
| 3170 setSelectionValue("createlink", value); |
| 3171 |
| 3172 // "Return true." |
| 3173 return true; |
| 3174 } |
| 3175 }; |
| 3176 |
| 3177 //@} |
| 3178 ///// The fontName command ///// |
| 3179 //@{ |
| 3180 commands.fontname = { |
| 3181 action: function(value) { |
| 3182 // "Set the selection's value to value, then return true." |
| 3183 setSelectionValue("fontname", value); |
| 3184 return true; |
| 3185 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily" |
| 3186 }; |
| 3187 |
| 3188 //@} |
| 3189 ///// The fontSize command ///// |
| 3190 //@{ |
| 3191 |
| 3192 // Helper function for fontSize's action plus queryOutputHelper. It's just the |
| 3193 // middle of fontSize's action, ripped out into its own function. Returns null |
| 3194 // if the size is invalid. |
| 3195 function normalizeFontSize(value) { |
| 3196 // "Strip leading and trailing whitespace from value." |
| 3197 // |
| 3198 // Cheap hack, not following the actual algorithm. |
| 3199 value = value.trim(); |
| 3200 |
| 3201 // "If value is not a valid floating point number, and would not be a valid |
| 3202 // floating point number if a single leading "+" character were stripped, |
| 3203 // return false." |
| 3204 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) { |
| 3205 return null; |
| 3206 } |
| 3207 |
| 3208 var mode; |
| 3209 |
| 3210 // "If the first character of value is "+", delete the character and let |
| 3211 // mode be "relative-plus"." |
| 3212 if (value[0] == "+") { |
| 3213 value = value.slice(1); |
| 3214 mode = "relative-plus"; |
| 3215 // "Otherwise, if the first character of value is "-", delete the character |
| 3216 // and let mode be "relative-minus"." |
| 3217 } else if (value[0] == "-") { |
| 3218 value = value.slice(1); |
| 3219 mode = "relative-minus"; |
| 3220 // "Otherwise, let mode be "absolute"." |
| 3221 } else { |
| 3222 mode = "absolute"; |
| 3223 } |
| 3224 |
| 3225 // "Apply the rules for parsing non-negative integers to value, and let |
| 3226 // number be the result." |
| 3227 // |
| 3228 // Another cheap hack. |
| 3229 var num = parseInt(value); |
| 3230 |
| 3231 // "If mode is "relative-plus", add three to number." |
| 3232 if (mode == "relative-plus") { |
| 3233 num += 3; |
| 3234 } |
| 3235 |
| 3236 // "If mode is "relative-minus", negate number, then add three to it." |
| 3237 if (mode == "relative-minus") { |
| 3238 num = 3 - num; |
| 3239 } |
| 3240 |
| 3241 // "If number is less than one, let number equal 1." |
| 3242 if (num < 1) { |
| 3243 num = 1; |
| 3244 } |
| 3245 |
| 3246 // "If number is greater than seven, let number equal 7." |
| 3247 if (num > 7) { |
| 3248 num = 7; |
| 3249 } |
| 3250 |
| 3251 // "Set value to the string here corresponding to number:" [table omitted] |
| 3252 value = { |
| 3253 1: "x-small", |
| 3254 2: "small", |
| 3255 3: "medium", |
| 3256 4: "large", |
| 3257 5: "x-large", |
| 3258 6: "xx-large", |
| 3259 7: "xxx-large" |
| 3260 }[num]; |
| 3261 |
| 3262 return value; |
| 3263 } |
| 3264 |
| 3265 commands.fontsize = { |
| 3266 action: function(value) { |
| 3267 value = normalizeFontSize(value); |
| 3268 if (value === null) { |
| 3269 return false; |
| 3270 } |
| 3271 |
| 3272 // "Set the selection's value to value." |
| 3273 setSelectionValue("fontsize", value); |
| 3274 |
| 3275 // "Return true." |
| 3276 return true; |
| 3277 }, indeterm: function() { |
| 3278 // "True if among formattable nodes that are effectively contained in |
| 3279 // the active range, there are two that have distinct effective command |
| 3280 // values. Otherwise false." |
| 3281 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNo
de) |
| 3282 .map(function(node) { |
| 3283 return getEffectiveCommandValue(node, "fontsize"); |
| 3284 }).filter(function(value, i, arr) { |
| 3285 return arr.slice(0, i).indexOf(value) == -1; |
| 3286 }).length >= 2; |
| 3287 }, value: function() { |
| 3288 // "If the active range is null, return the empty string." |
| 3289 if (!getActiveRange()) { |
| 3290 return ""; |
| 3291 } |
| 3292 |
| 3293 // "Let pixel size be the effective command value of the first |
| 3294 // formattable node that is effectively contained in the active range, |
| 3295 // or if there is no such node, the effective command value of the |
| 3296 // active range's start node, in either case interpreted as a number of |
| 3297 // pixels." |
| 3298 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattab
leNode)[0]; |
| 3299 if (node === undefined) { |
| 3300 node = getActiveRange().startContainer; |
| 3301 } |
| 3302 var pixelSize = getEffectiveCommandValue(node, "fontsize"); |
| 3303 |
| 3304 // "Return the legacy font size for pixel size." |
| 3305 return getLegacyFontSize(pixelSize); |
| 3306 }, relevantCssProperty: "fontSize" |
| 3307 }; |
| 3308 |
| 3309 function getLegacyFontSize(size) { |
| 3310 if (getLegacyFontSize.resultCache === undefined) { |
| 3311 getLegacyFontSize.resultCache = {}; |
| 3312 } |
| 3313 |
| 3314 if (getLegacyFontSize.resultCache[size] !== undefined) { |
| 3315 return getLegacyFontSize.resultCache[size]; |
| 3316 } |
| 3317 |
| 3318 // For convenience in other places in my code, I handle all sizes, not just |
| 3319 // pixel sizes as the spec says. This means pixel sizes have to be passed |
| 3320 // in suffixed with "px", not as plain numbers. |
| 3321 if (normalizeFontSize(size) !== null) { |
| 3322 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFo
ntSize(size)); |
| 3323 } |
| 3324 |
| 3325 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large"
, "xxx-large"].indexOf(size) == -1 |
| 3326 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { |
| 3327 // There is no sensible legacy size for things like "2em". |
| 3328 return getLegacyFontSize.resultCache[size] = null; |
| 3329 } |
| 3330 |
| 3331 var font = document.createElement("font"); |
| 3332 document.body.appendChild(font); |
| 3333 if (size == "xxx-large") { |
| 3334 font.size = 7; |
| 3335 } else { |
| 3336 font.style.fontSize = size; |
| 3337 } |
| 3338 var pixelSize = parseInt(getComputedStyle(font).fontSize); |
| 3339 document.body.removeChild(font); |
| 3340 |
| 3341 // "Let returned size be 1." |
| 3342 var returnedSize = 1; |
| 3343 |
| 3344 // "While returned size is less than 7:" |
| 3345 while (returnedSize < 7) { |
| 3346 // "Let lower bound be the resolved value of "font-size" in pixels |
| 3347 // of a font element whose size attribute is set to returned size." |
| 3348 var font = document.createElement("font"); |
| 3349 font.size = returnedSize; |
| 3350 document.body.appendChild(font); |
| 3351 var lowerBound = parseInt(getComputedStyle(font).fontSize); |
| 3352 |
| 3353 // "Let upper bound be the resolved value of "font-size" in pixels |
| 3354 // of a font element whose size attribute is set to one plus |
| 3355 // returned size." |
| 3356 font.size = 1 + returnedSize; |
| 3357 var upperBound = parseInt(getComputedStyle(font).fontSize); |
| 3358 document.body.removeChild(font); |
| 3359 |
| 3360 // "Let average be the average of upper bound and lower bound." |
| 3361 var average = (upperBound + lowerBound)/2; |
| 3362 |
| 3363 // "If pixel size is less than average, return the one-element |
| 3364 // string consisting of the digit returned size." |
| 3365 if (pixelSize < average) { |
| 3366 return getLegacyFontSize.resultCache[size] = String(returnedSize); |
| 3367 } |
| 3368 |
| 3369 // "Add one to returned size." |
| 3370 returnedSize++; |
| 3371 } |
| 3372 |
| 3373 // "Return "7"." |
| 3374 return getLegacyFontSize.resultCache[size] = "7"; |
| 3375 } |
| 3376 |
| 3377 //@} |
| 3378 ///// The foreColor command ///// |
| 3379 //@{ |
| 3380 commands.forecolor = { |
| 3381 action: function(value) { |
| 3382 // Copy-pasted, same as backColor and hiliteColor |
| 3383 |
| 3384 // "If value is not a valid CSS color, prepend "#" to it." |
| 3385 // |
| 3386 // "If value is still not a valid CSS color, or if it is currentColor, |
| 3387 // return false." |
| 3388 // |
| 3389 // Cheap hack for testing, no attempt to be comprehensive. |
| 3390 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
| 3391 value = "#" + value; |
| 3392 } |
| 3393 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
| 3394 && !parseSimpleColor(value) |
| 3395 && value.toLowerCase() != "transparent") { |
| 3396 return false; |
| 3397 } |
| 3398 |
| 3399 // "Set the selection's value to value." |
| 3400 setSelectionValue("forecolor", value); |
| 3401 |
| 3402 // "Return true." |
| 3403 return true; |
| 3404 }, standardInlineValueCommand: true, relevantCssProperty: "color", |
| 3405 equivalentValues: function(val1, val2) { |
| 3406 // "Either both strings are valid CSS colors and have the same red, |
| 3407 // green, blue, and alpha components, or neither string is a valid CSS |
| 3408 // color." |
| 3409 return normalizeColor(val1) === normalizeColor(val2); |
| 3410 }, |
| 3411 }; |
| 3412 |
| 3413 //@} |
| 3414 ///// The hiliteColor command ///// |
| 3415 //@{ |
| 3416 commands.hilitecolor = { |
| 3417 // Copy-pasted, same as backColor |
| 3418 action: function(value) { |
| 3419 // Action is further copy-pasted, same as foreColor |
| 3420 |
| 3421 // "If value is not a valid CSS color, prepend "#" to it." |
| 3422 // |
| 3423 // "If value is still not a valid CSS color, or if it is currentColor, |
| 3424 // return false." |
| 3425 // |
| 3426 // Cheap hack for testing, no attempt to be comprehensive. |
| 3427 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
| 3428 value = "#" + value; |
| 3429 } |
| 3430 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
| 3431 && !parseSimpleColor(value) |
| 3432 && value.toLowerCase() != "transparent") { |
| 3433 return false; |
| 3434 } |
| 3435 |
| 3436 // "Set the selection's value to value." |
| 3437 setSelectionValue("hilitecolor", value); |
| 3438 |
| 3439 // "Return true." |
| 3440 return true; |
| 3441 }, indeterm: function() { |
| 3442 // "True if among editable Text nodes that are effectively contained in |
| 3443 // the active range, there are two that have distinct effective command |
| 3444 // values. Otherwise false." |
| 3445 return getAllEffectivelyContainedNodes(getActiveRange(), function(node)
{ |
| 3446 return isEditable(node) && node.nodeType == Node.TEXT_NODE; |
| 3447 }).map(function(node) { |
| 3448 return getEffectiveCommandValue(node, "hilitecolor"); |
| 3449 }).filter(function(value, i, arr) { |
| 3450 return arr.slice(0, i).indexOf(value) == -1; |
| 3451 }).length >= 2; |
| 3452 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
| 3453 equivalentValues: function(val1, val2) { |
| 3454 // "Either both strings are valid CSS colors and have the same red, |
| 3455 // green, blue, and alpha components, or neither string is a valid CSS |
| 3456 // color." |
| 3457 return normalizeColor(val1) === normalizeColor(val2); |
| 3458 }, |
| 3459 }; |
| 3460 |
| 3461 //@} |
| 3462 ///// The italic command ///// |
| 3463 //@{ |
| 3464 commands.italic = { |
| 3465 action: function() { |
| 3466 // "If queryCommandState("italic") returns true, set the selection's |
| 3467 // value to "normal". Otherwise set the selection's value to "italic". |
| 3468 // Either way, return true." |
| 3469 if (myQueryCommandState("italic")) { |
| 3470 setSelectionValue("italic", "normal"); |
| 3471 } else { |
| 3472 setSelectionValue("italic", "italic"); |
| 3473 } |
| 3474 return true; |
| 3475 }, inlineCommandActivatedValues: ["italic", "oblique"], |
| 3476 relevantCssProperty: "fontStyle" |
| 3477 }; |
| 3478 |
| 3479 //@} |
| 3480 ///// The removeFormat command ///// |
| 3481 //@{ |
| 3482 commands.removeformat = { |
| 3483 action: function() { |
| 3484 // "A removeFormat candidate is an editable HTML element with local |
| 3485 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", |
| 3486 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", |
| 3487 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", |
| 3488 // "tt", "u", or "var"." |
| 3489 function isRemoveFormatCandidate(node) { |
| 3490 return isEditable(node) |
| 3491 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo", |
| 3492 "big", "blink", "cite", "code", "dfn", "em", "font", "i", |
| 3493 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", |
| 3494 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); |
| 3495 } |
| 3496 |
| 3497 // "Let elements to remove be a list of every removeFormat candidate |
| 3498 // effectively contained in the active range." |
| 3499 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(),
isRemoveFormatCandidate); |
| 3500 |
| 3501 // "For each element in elements to remove:" |
| 3502 elementsToRemove.forEach(function(element) { |
| 3503 // "While element has children, insert the first child of element |
| 3504 // into the parent of element immediately before element, |
| 3505 // preserving ranges." |
| 3506 while (element.hasChildNodes()) { |
| 3507 movePreservingRanges(element.firstChild, element.parentNode, get
NodeIndex(element)); |
| 3508 } |
| 3509 |
| 3510 // "Remove element from its parent." |
| 3511 element.parentNode.removeChild(element); |
| 3512 }); |
| 3513 |
| 3514 // "If the active range's start node is an editable Text node, and its |
| 3515 // start offset is neither zero nor its start node's length, call |
| 3516 // splitText() on the active range's start node, with argument equal to |
| 3517 // the active range's start offset. Then set the active range's start |
| 3518 // node to the result, and its start offset to zero." |
| 3519 if (isEditable(getActiveRange().startContainer) |
| 3520 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 3521 && getActiveRange().startOffset != 0 |
| 3522 && getActiveRange().startOffset != getNodeLength(getActiveRange().startC
ontainer)) { |
| 3523 // Account for browsers not following range mutation rules |
| 3524 if (getActiveRange().startContainer == getActiveRange().endContainer
) { |
| 3525 var newEnd = getActiveRange().endOffset - getActiveRange().start
Offset; |
| 3526 var newNode = getActiveRange().startContainer.splitText(getActiv
eRange().startOffset); |
| 3527 getActiveRange().setStart(newNode, 0); |
| 3528 getActiveRange().setEnd(newNode, newEnd); |
| 3529 } else { |
| 3530 getActiveRange().setStart(getActiveRange().startContainer.splitT
ext(getActiveRange().startOffset), 0); |
| 3531 } |
| 3532 } |
| 3533 |
| 3534 // "If the active range's end node is an editable Text node, and its |
| 3535 // end offset is neither zero nor its end node's length, call |
| 3536 // splitText() on the active range's end node, with argument equal to |
| 3537 // the active range's end offset." |
| 3538 if (isEditable(getActiveRange().endContainer) |
| 3539 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
| 3540 && getActiveRange().endOffset != 0 |
| 3541 && getActiveRange().endOffset != getNodeLength(getActiveRange().endConta
iner)) { |
| 3542 // IE seems to mutate the range incorrectly here, so we need |
| 3543 // correction here as well. Have to be careful to set the range to |
| 3544 // something not including the text node so that getActiveRange() |
| 3545 // doesn't throw an exception due to a temporarily detached |
| 3546 // endpoint. |
| 3547 var newStart = [getActiveRange().startContainer, getActiveRange().st
artOffset]; |
| 3548 var newEnd = [getActiveRange().endContainer, getActiveRange().endOff
set]; |
| 3549 getActiveRange().setEnd(document.documentElement, 0); |
| 3550 newEnd[0].splitText(newEnd[1]); |
| 3551 getActiveRange().setStart(newStart[0], newStart[1]); |
| 3552 getActiveRange().setEnd(newEnd[0], newEnd[1]); |
| 3553 } |
| 3554 |
| 3555 // "Let node list consist of all editable nodes effectively contained |
| 3556 // in the active range." |
| 3557 // |
| 3558 // "For each node in node list, while node's parent is a removeFormat |
| 3559 // candidate in the same editing host as node, split the parent of the |
| 3560 // one-node list consisting of node." |
| 3561 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(fu
nction(node) { |
| 3562 while (isRemoveFormatCandidate(node.parentNode) |
| 3563 && inSameEditingHost(node.parentNode, node)) { |
| 3564 splitParent([node]); |
| 3565 } |
| 3566 }); |
| 3567 |
| 3568 // "For each of the entries in the following list, in the given order, |
| 3569 // set the selection's value to null, with command as given." |
| 3570 [ |
| 3571 "subscript", |
| 3572 "bold", |
| 3573 "fontname", |
| 3574 "fontsize", |
| 3575 "forecolor", |
| 3576 "hilitecolor", |
| 3577 "italic", |
| 3578 "strikethrough", |
| 3579 "underline", |
| 3580 ].forEach(function(command) { |
| 3581 setSelectionValue(command, null); |
| 3582 }); |
| 3583 |
| 3584 // "Return true." |
| 3585 return true; |
| 3586 } |
| 3587 }; |
| 3588 |
| 3589 //@} |
| 3590 ///// The strikethrough command ///// |
| 3591 //@{ |
| 3592 commands.strikethrough = { |
| 3593 action: function() { |
| 3594 // "If queryCommandState("strikethrough") returns true, set the |
| 3595 // selection's value to null. Otherwise set the selection's value to |
| 3596 // "line-through". Either way, return true." |
| 3597 if (myQueryCommandState("strikethrough")) { |
| 3598 setSelectionValue("strikethrough", null); |
| 3599 } else { |
| 3600 setSelectionValue("strikethrough", "line-through"); |
| 3601 } |
| 3602 return true; |
| 3603 }, inlineCommandActivatedValues: ["line-through"] |
| 3604 }; |
| 3605 |
| 3606 //@} |
| 3607 ///// The subscript command ///// |
| 3608 //@{ |
| 3609 commands.subscript = { |
| 3610 action: function() { |
| 3611 // "Call queryCommandState("subscript"), and let state be the result." |
| 3612 var state = myQueryCommandState("subscript"); |
| 3613 |
| 3614 // "Set the selection's value to null." |
| 3615 setSelectionValue("subscript", null); |
| 3616 |
| 3617 // "If state is false, set the selection's value to "subscript"." |
| 3618 if (!state) { |
| 3619 setSelectionValue("subscript", "subscript"); |
| 3620 } |
| 3621 |
| 3622 // "Return true." |
| 3623 return true; |
| 3624 }, indeterm: function() { |
| 3625 // "True if either among formattable nodes that are effectively |
| 3626 // contained in the active range, there is at least one with effective |
| 3627 // command value "subscript" and at least one with some other effective |
| 3628 // command value; or if there is some formattable node effectively |
| 3629 // contained in the active range with effective command value "mixed". |
| 3630 // Otherwise false." |
| 3631 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormatta
bleNode); |
| 3632 return (nodes.some(function(node) { return getEffectiveCommandValue(node
, "subscript") == "subscript" }) |
| 3633 && nodes.some(function(node) { return getEffectiveCommandValue(node,
"subscript") != "subscript" })) |
| 3634 || nodes.some(function(node) { return getEffectiveCommandValue(node,
"subscript") == "mixed" }); |
| 3635 }, inlineCommandActivatedValues: ["subscript"], |
| 3636 }; |
| 3637 |
| 3638 //@} |
| 3639 ///// The superscript command ///// |
| 3640 //@{ |
| 3641 commands.superscript = { |
| 3642 action: function() { |
| 3643 // "Call queryCommandState("superscript"), and let state be the |
| 3644 // result." |
| 3645 var state = myQueryCommandState("superscript"); |
| 3646 |
| 3647 // "Set the selection's value to null." |
| 3648 setSelectionValue("superscript", null); |
| 3649 |
| 3650 // "If state is false, set the selection's value to "superscript"." |
| 3651 if (!state) { |
| 3652 setSelectionValue("superscript", "superscript"); |
| 3653 } |
| 3654 |
| 3655 // "Return true." |
| 3656 return true; |
| 3657 }, indeterm: function() { |
| 3658 // "True if either among formattable nodes that are effectively |
| 3659 // contained in the active range, there is at least one with effective |
| 3660 // command value "superscript" and at least one with some other |
| 3661 // effective command value; or if there is some formattable node |
| 3662 // effectively contained in the active range with effective command |
| 3663 // value "mixed". Otherwise false." |
| 3664 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormatta
bleNode); |
| 3665 return (nodes.some(function(node) { return getEffectiveCommandValue(node
, "superscript") == "superscript" }) |
| 3666 && nodes.some(function(node) { return getEffectiveCommandValue(node,
"superscript") != "superscript" })) |
| 3667 || nodes.some(function(node) { return getEffectiveCommandValue(node,
"superscript") == "mixed" }); |
| 3668 }, inlineCommandActivatedValues: ["superscript"], |
| 3669 }; |
| 3670 |
| 3671 //@} |
| 3672 ///// The underline command ///// |
| 3673 //@{ |
| 3674 commands.underline = { |
| 3675 action: function() { |
| 3676 // "If queryCommandState("underline") returns true, set the selection's |
| 3677 // value to null. Otherwise set the selection's value to "underline". |
| 3678 // Either way, return true." |
| 3679 if (myQueryCommandState("underline")) { |
| 3680 setSelectionValue("underline", null); |
| 3681 } else { |
| 3682 setSelectionValue("underline", "underline"); |
| 3683 } |
| 3684 return true; |
| 3685 }, inlineCommandActivatedValues: ["underline"] |
| 3686 }; |
| 3687 |
| 3688 //@} |
| 3689 ///// The unlink command ///// |
| 3690 //@{ |
| 3691 commands.unlink = { |
| 3692 action: function() { |
| 3693 // "Let hyperlinks be a list of every a element that has an href |
| 3694 // attribute and is contained in the active range or is an ancestor of |
| 3695 // one of its boundary points." |
| 3696 // |
| 3697 // As usual, take care to ensure it's tree order. The correctness of |
| 3698 // the following is left as an exercise for the reader. |
| 3699 var range = getActiveRange(); |
| 3700 var hyperlinks = []; |
| 3701 for ( |
| 3702 var node = range.startContainer; |
| 3703 node; |
| 3704 node = node.parentNode |
| 3705 ) { |
| 3706 if (isHtmlElement(node, "A") |
| 3707 && node.hasAttribute("href")) { |
| 3708 hyperlinks.unshift(node); |
| 3709 } |
| 3710 } |
| 3711 for ( |
| 3712 var node = range.startContainer; |
| 3713 node != nextNodeDescendants(range.endContainer); |
| 3714 node = nextNode(node) |
| 3715 ) { |
| 3716 if (isHtmlElement(node, "A") |
| 3717 && node.hasAttribute("href") |
| 3718 && (isContained(node, range) |
| 3719 || isAncestor(node, range.endContainer) |
| 3720 || node == range.endContainer)) { |
| 3721 hyperlinks.push(node); |
| 3722 } |
| 3723 } |
| 3724 |
| 3725 // "Clear the value of each member of hyperlinks." |
| 3726 for (var i = 0; i < hyperlinks.length; i++) { |
| 3727 clearValue(hyperlinks[i], "unlink"); |
| 3728 } |
| 3729 |
| 3730 // "Return true." |
| 3731 return true; |
| 3732 } |
| 3733 }; |
| 3734 |
| 3735 //@} |
| 3736 |
| 3737 ///////////////////////////////////// |
| 3738 ///// Block formatting commands ///// |
| 3739 ///////////////////////////////////// |
| 3740 |
| 3741 ///// Block formatting command definitions ///// |
| 3742 //@{ |
| 3743 |
| 3744 // "An indentation element is either a blockquote, or a div that has a style |
| 3745 // attribute that sets "margin" or some subproperty of it." |
| 3746 function isIndentationElement(node) { |
| 3747 if (!isHtmlElement(node)) { |
| 3748 return false; |
| 3749 } |
| 3750 |
| 3751 if (node.tagName == "BLOCKQUOTE") { |
| 3752 return true; |
| 3753 } |
| 3754 |
| 3755 if (node.tagName != "DIV") { |
| 3756 return false; |
| 3757 } |
| 3758 |
| 3759 for (var i = 0; i < node.style.length; i++) { |
| 3760 // Approximate check |
| 3761 if (/^(-[a-z]+-)?margin/.test(node.style[i])) { |
| 3762 return true; |
| 3763 } |
| 3764 } |
| 3765 |
| 3766 return false; |
| 3767 } |
| 3768 |
| 3769 // "A simple indentation element is an indentation element that has no |
| 3770 // attributes except possibly |
| 3771 // |
| 3772 // * "a style attribute that sets no properties other than "margin", |
| 3773 // "border", "padding", or subproperties of those; and/or |
| 3774 // * "a dir attribute." |
| 3775 function isSimpleIndentationElement(node) { |
| 3776 if (!isIndentationElement(node)) { |
| 3777 return false; |
| 3778 } |
| 3779 |
| 3780 for (var i = 0; i < node.attributes.length; i++) { |
| 3781 if (!isHtmlNamespace(node.attributes[i].namespaceURI) |
| 3782 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) { |
| 3783 return false; |
| 3784 } |
| 3785 } |
| 3786 |
| 3787 for (var i = 0; i < node.style.length; i++) { |
| 3788 // This is approximate, but it works well enough for my purposes. |
| 3789 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { |
| 3790 return false; |
| 3791 } |
| 3792 } |
| 3793 |
| 3794 return true; |
| 3795 } |
| 3796 |
| 3797 // "A non-list single-line container is an HTML element with local name |
| 3798 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", |
| 3799 // or "xmp"." |
| 3800 function isNonListSingleLineContainer(node) { |
| 3801 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", |
| 3802 "h6", "listing", "p", "pre", "xmp"]); |
| 3803 } |
| 3804 |
| 3805 // "A single-line container is either a non-list single-line container, or an |
| 3806 // HTML element with local name "li", "dt", or "dd"." |
| 3807 function isSingleLineContainer(node) { |
| 3808 return isNonListSingleLineContainer(node) |
| 3809 || isHtmlElement(node, ["li", "dt", "dd"]); |
| 3810 } |
| 3811 |
| 3812 function getBlockNodeOf(node) { |
| 3813 // "While node is an inline node, set node to its parent." |
| 3814 while (isInlineNode(node)) { |
| 3815 node = node.parentNode; |
| 3816 } |
| 3817 |
| 3818 // "Return node." |
| 3819 return node; |
| 3820 } |
| 3821 |
| 3822 //@} |
| 3823 ///// Assorted block formatting command algorithms ///// |
| 3824 //@{ |
| 3825 |
| 3826 function fixDisallowedAncestors(node) { |
| 3827 // "If node is not editable, abort these steps." |
| 3828 if (!isEditable(node)) { |
| 3829 return; |
| 3830 } |
| 3831 |
| 3832 // "If node is not an allowed child of any of its ancestors in the same |
| 3833 // editing host:" |
| 3834 if (getAncestors(node).every(function(ancestor) { |
| 3835 return !inSameEditingHost(node, ancestor) |
| 3836 || !isAllowedChild(node, ancestor) |
| 3837 })) { |
| 3838 // "If node is a dd or dt, wrap the one-node list consisting of node, |
| 3839 // with sibling criteria returning true for any dl with no attributes |
| 3840 // and false otherwise, and new parent instructions returning the |
| 3841 // result of calling createElement("dl") on the context object. Then |
| 3842 // abort these steps." |
| 3843 if (isHtmlElement(node, ["dd", "dt"])) { |
| 3844 wrap([node], |
| 3845 function(sibling) { return isHtmlElement(sibling, "dl") && !sibl
ing.attributes.length }, |
| 3846 function() { return document.createElement("dl") }); |
| 3847 return; |
| 3848 } |
| 3849 |
| 3850 // "If "p" is not an allowed child of the editing host of node, abort |
| 3851 // these steps." |
| 3852 if (!isAllowedChild("p", getEditingHostOf(node))) { |
| 3853 return; |
| 3854 } |
| 3855 |
| 3856 // "If node is not a prohibited paragraph child, abort these steps." |
| 3857 if (!isProhibitedParagraphChild(node)) { |
| 3858 return; |
| 3859 } |
| 3860 |
| 3861 // "Set the tag name of node to the default single-line container name, |
| 3862 // and let node be the result." |
| 3863 node = setTagName(node, defaultSingleLineContainerName); |
| 3864 |
| 3865 // "Fix disallowed ancestors of node." |
| 3866 fixDisallowedAncestors(node); |
| 3867 |
| 3868 // "Let children be node's children." |
| 3869 var children = [].slice.call(node.childNodes); |
| 3870 |
| 3871 // "For each child in children, if child is a prohibited paragraph |
| 3872 // child:" |
| 3873 children.filter(isProhibitedParagraphChild) |
| 3874 .forEach(function(child) { |
| 3875 // "Record the values of the one-node list consisting of child, and |
| 3876 // let values be the result." |
| 3877 var values = recordValues([child]); |
| 3878 |
| 3879 // "Split the parent of the one-node list consisting of child." |
| 3880 splitParent([child]); |
| 3881 |
| 3882 // "Restore the values from values." |
| 3883 restoreValues(values); |
| 3884 }); |
| 3885 |
| 3886 // "Abort these steps." |
| 3887 return; |
| 3888 } |
| 3889 |
| 3890 // "Record the values of the one-node list consisting of node, and let |
| 3891 // values be the result." |
| 3892 var values = recordValues([node]); |
| 3893 |
| 3894 // "While node is not an allowed child of its parent, split the parent of |
| 3895 // the one-node list consisting of node." |
| 3896 while (!isAllowedChild(node, node.parentNode)) { |
| 3897 splitParent([node]); |
| 3898 } |
| 3899 |
| 3900 // "Restore the values from values." |
| 3901 restoreValues(values); |
| 3902 } |
| 3903 |
| 3904 function normalizeSublists(item) { |
| 3905 // "If item is not an li or it is not editable or its parent is not |
| 3906 // editable, abort these steps." |
| 3907 if (!isHtmlElement(item, "LI") |
| 3908 || !isEditable(item) |
| 3909 || !isEditable(item.parentNode)) { |
| 3910 return; |
| 3911 } |
| 3912 |
| 3913 // "Let new item be null." |
| 3914 var newItem = null; |
| 3915 |
| 3916 // "While item has an ol or ul child:" |
| 3917 while ([].some.call(item.childNodes, function (node) { return isHtmlElement(
node, ["OL", "UL"]) })) { |
| 3918 // "Let child be the last child of item." |
| 3919 var child = item.lastChild; |
| 3920 |
| 3921 // "If child is an ol or ul, or new item is null and child is a Text |
| 3922 // node whose data consists of zero of more space characters:" |
| 3923 if (isHtmlElement(child, ["OL", "UL"]) |
| 3924 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.tes
t(child.data))) { |
| 3925 // "Set new item to null." |
| 3926 newItem = null; |
| 3927 |
| 3928 // "Insert child into the parent of item immediately following |
| 3929 // item, preserving ranges." |
| 3930 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item))
; |
| 3931 |
| 3932 // "Otherwise:" |
| 3933 } else { |
| 3934 // "If new item is null, let new item be the result of calling |
| 3935 // createElement("li") on the ownerDocument of item, then insert |
| 3936 // new item into the parent of item immediately after item." |
| 3937 if (!newItem) { |
| 3938 newItem = item.ownerDocument.createElement("li"); |
| 3939 item.parentNode.insertBefore(newItem, item.nextSibling); |
| 3940 } |
| 3941 |
| 3942 // "Insert child into new item as its first child, preserving |
| 3943 // ranges." |
| 3944 movePreservingRanges(child, newItem, 0); |
| 3945 } |
| 3946 } |
| 3947 } |
| 3948 |
| 3949 function getSelectionListState() { |
| 3950 // "If the active range is null, return "none"." |
| 3951 if (!getActiveRange()) { |
| 3952 return "none"; |
| 3953 } |
| 3954 |
| 3955 // "Block-extend the active range, and let new range be the result." |
| 3956 var newRange = blockExtend(getActiveRange()); |
| 3957 |
| 3958 // "Let node list be a list of nodes, initially empty." |
| 3959 // |
| 3960 // "For each node contained in new range, append node to node list if the |
| 3961 // last member of node list (if any) is not an ancestor of node; node is |
| 3962 // editable; node is not an indentation element; and node is either an ol |
| 3963 // or ul, or the child of an ol or ul, or an allowed child of "li"." |
| 3964 var nodeList = getContainedNodes(newRange, function(node) { |
| 3965 return isEditable(node) |
| 3966 && !isIndentationElement(node) |
| 3967 && (isHtmlElement(node, ["ol", "ul"]) |
| 3968 || isHtmlElement(node.parentNode, ["ol", "ul"]) |
| 3969 || isAllowedChild(node, "li")); |
| 3970 }); |
| 3971 |
| 3972 // "If node list is empty, return "none"." |
| 3973 if (!nodeList.length) { |
| 3974 return "none"; |
| 3975 } |
| 3976 |
| 3977 // "If every member of node list is either an ol or the child of an ol or |
| 3978 // the child of an li child of an ol, and none is a ul or an ancestor of a |
| 3979 // ul, return "ol"." |
| 3980 if (nodeList.every(function(node) { |
| 3981 return isHtmlElement(node, "ol") |
| 3982 || isHtmlElement(node.parentNode, "ol") |
| 3983 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren
tNode.parentNode, "ol")); |
| 3984 }) |
| 3985 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("que
rySelector" in node && node.querySelector("ul")) })) { |
| 3986 return "ol"; |
| 3987 } |
| 3988 |
| 3989 // "If every member of node list is either a ul or the child of a ul or the |
| 3990 // child of an li child of a ul, and none is an ol or an ancestor of an ol, |
| 3991 // return "ul"." |
| 3992 if (nodeList.every(function(node) { |
| 3993 return isHtmlElement(node, "ul") |
| 3994 || isHtmlElement(node.parentNode, "ul") |
| 3995 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren
tNode.parentNode, "ul")); |
| 3996 }) |
| 3997 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("que
rySelector" in node && node.querySelector("ol")) })) { |
| 3998 return "ul"; |
| 3999 } |
| 4000 |
| 4001 var hasOl = nodeList.some(function(node) { |
| 4002 return isHtmlElement(node, "ol") |
| 4003 || isHtmlElement(node.parentNode, "ol") |
| 4004 || ("querySelector" in node && node.querySelector("ol")) |
| 4005 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren
tNode.parentNode, "ol")); |
| 4006 }); |
| 4007 var hasUl = nodeList.some(function(node) { |
| 4008 return isHtmlElement(node, "ul") |
| 4009 || isHtmlElement(node.parentNode, "ul") |
| 4010 || ("querySelector" in node && node.querySelector("ul")) |
| 4011 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren
tNode.parentNode, "ul")); |
| 4012 }); |
| 4013 // "If some member of node list is either an ol or the child or ancestor of |
| 4014 // an ol or the child of an li child of an ol, and some member of node list |
| 4015 // is either a ul or the child or ancestor of a ul or the child of an li |
| 4016 // child of a ul, return "mixed"." |
| 4017 if (hasOl && hasUl) { |
| 4018 return "mixed"; |
| 4019 } |
| 4020 |
| 4021 // "If some member of node list is either an ol or the child or ancestor of |
| 4022 // an ol or the child of an li child of an ol, return "mixed ol"." |
| 4023 if (hasOl) { |
| 4024 return "mixed ol"; |
| 4025 } |
| 4026 |
| 4027 // "If some member of node list is either a ul or the child or ancestor of |
| 4028 // a ul or the child of an li child of a ul, return "mixed ul"." |
| 4029 if (hasUl) { |
| 4030 return "mixed ul"; |
| 4031 } |
| 4032 |
| 4033 // "Return "none"." |
| 4034 return "none"; |
| 4035 } |
| 4036 |
| 4037 function getAlignmentValue(node) { |
| 4038 // "While node is neither null nor an Element, or it is an Element but its |
| 4039 // "display" property has resolved value "inline" or "none", set node to |
| 4040 // its parent." |
| 4041 while ((node && node.nodeType != Node.ELEMENT_NODE) |
| 4042 || (node.nodeType == Node.ELEMENT_NODE |
| 4043 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) { |
| 4044 node = node.parentNode; |
| 4045 } |
| 4046 |
| 4047 // "If node is not an Element, return "left"." |
| 4048 if (!node || node.nodeType != Node.ELEMENT_NODE) { |
| 4049 return "left"; |
| 4050 } |
| 4051 |
| 4052 var resolvedValue = getComputedStyle(node).textAlign |
| 4053 // Hack around browser non-standardness |
| 4054 .replace(/^-(moz|webkit)-/, "") |
| 4055 .replace(/^auto$/, "start"); |
| 4056 |
| 4057 // "If node's "text-align" property has resolved value "start", return |
| 4058 // "left" if the directionality of node is "ltr", "right" if it is "rtl"." |
| 4059 if (resolvedValue == "start") { |
| 4060 return getDirectionality(node) == "ltr" ? "left" : "right"; |
| 4061 } |
| 4062 |
| 4063 // "If node's "text-align" property has resolved value "end", return |
| 4064 // "right" if the directionality of node is "ltr", "left" if it is "rtl"." |
| 4065 if (resolvedValue == "end") { |
| 4066 return getDirectionality(node) == "ltr" ? "right" : "left"; |
| 4067 } |
| 4068 |
| 4069 // "If node's "text-align" property has resolved value "center", "justify", |
| 4070 // "left", or "right", return that value." |
| 4071 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) { |
| 4072 return resolvedValue; |
| 4073 } |
| 4074 |
| 4075 // "Return "left"." |
| 4076 return "left"; |
| 4077 } |
| 4078 |
| 4079 function getNextEquivalentPoint(node, offset) { |
| 4080 // "If node's length is zero, return null." |
| 4081 if (getNodeLength(node) == 0) { |
| 4082 return null; |
| 4083 } |
| 4084 |
| 4085 // "If offset is node's length, and node's parent is not null, and node is |
| 4086 // an inline node, return (node's parent, 1 + node's index)." |
| 4087 if (offset == getNodeLength(node) |
| 4088 && node.parentNode |
| 4089 && isInlineNode(node)) { |
| 4090 return [node.parentNode, 1 + getNodeIndex(node)]; |
| 4091 } |
| 4092 |
| 4093 // "If node has a child with index offset, and that child's length is not |
| 4094 // zero, and that child is an inline node, return (that child, 0)." |
| 4095 if (0 <= offset |
| 4096 && offset < node.childNodes.length |
| 4097 && getNodeLength(node.childNodes[offset]) != 0 |
| 4098 && isInlineNode(node.childNodes[offset])) { |
| 4099 return [node.childNodes[offset], 0]; |
| 4100 } |
| 4101 |
| 4102 // "Return null." |
| 4103 return null; |
| 4104 } |
| 4105 |
| 4106 function getPreviousEquivalentPoint(node, offset) { |
| 4107 // "If node's length is zero, return null." |
| 4108 if (getNodeLength(node) == 0) { |
| 4109 return null; |
| 4110 } |
| 4111 |
| 4112 // "If offset is 0, and node's parent is not null, and node is an inline |
| 4113 // node, return (node's parent, node's index)." |
| 4114 if (offset == 0 |
| 4115 && node.parentNode |
| 4116 && isInlineNode(node)) { |
| 4117 return [node.parentNode, getNodeIndex(node)]; |
| 4118 } |
| 4119 |
| 4120 // "If node has a child with index offset − 1, and that child's length is |
| 4121 // not zero, and that child is an inline node, return (that child, that |
| 4122 // child's length)." |
| 4123 if (0 <= offset - 1 |
| 4124 && offset - 1 < node.childNodes.length |
| 4125 && getNodeLength(node.childNodes[offset - 1]) != 0 |
| 4126 && isInlineNode(node.childNodes[offset - 1])) { |
| 4127 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offse
t - 1])]; |
| 4128 } |
| 4129 |
| 4130 // "Return null." |
| 4131 return null; |
| 4132 } |
| 4133 |
| 4134 function getFirstEquivalentPoint(node, offset) { |
| 4135 // "While (node, offset)'s previous equivalent point is not null, set |
| 4136 // (node, offset) to its previous equivalent point." |
| 4137 var prev; |
| 4138 while (prev = getPreviousEquivalentPoint(node, offset)) { |
| 4139 node = prev[0]; |
| 4140 offset = prev[1]; |
| 4141 } |
| 4142 |
| 4143 // "Return (node, offset)." |
| 4144 return [node, offset]; |
| 4145 } |
| 4146 |
| 4147 function getLastEquivalentPoint(node, offset) { |
| 4148 // "While (node, offset)'s next equivalent point is not null, set (node, |
| 4149 // offset) to its next equivalent point." |
| 4150 var next; |
| 4151 while (next = getNextEquivalentPoint(node, offset)) { |
| 4152 node = next[0]; |
| 4153 offset = next[1]; |
| 4154 } |
| 4155 |
| 4156 // "Return (node, offset)." |
| 4157 return [node, offset]; |
| 4158 } |
| 4159 |
| 4160 //@} |
| 4161 ///// Block-extending a range ///// |
| 4162 //@{ |
| 4163 |
| 4164 // "A boundary point (node, offset) is a block start point if either node's |
| 4165 // parent is null and offset is zero; or node has a child with index offset − |
| 4166 // 1, and that child is either a visible block node or a visible br." |
| 4167 function isBlockStartPoint(node, offset) { |
| 4168 return (!node.parentNode && offset == 0) |
| 4169 || (0 <= offset - 1 |
| 4170 && offset - 1 < node.childNodes.length |
| 4171 && isVisible(node.childNodes[offset - 1]) |
| 4172 && (isBlockNode(node.childNodes[offset - 1]) |
| 4173 || isHtmlElement(node.childNodes[offset - 1], "br"))); |
| 4174 } |
| 4175 |
| 4176 // "A boundary point (node, offset) is a block end point if either node's |
| 4177 // parent is null and offset is node's length; or node has a child with index |
| 4178 // offset, and that child is a visible block node." |
| 4179 function isBlockEndPoint(node, offset) { |
| 4180 return (!node.parentNode && offset == getNodeLength(node)) |
| 4181 || (offset < node.childNodes.length |
| 4182 && isVisible(node.childNodes[offset]) |
| 4183 && isBlockNode(node.childNodes[offset])); |
| 4184 } |
| 4185 |
| 4186 // "A boundary point is a block boundary point if it is either a block start |
| 4187 // point or a block end point." |
| 4188 function isBlockBoundaryPoint(node, offset) { |
| 4189 return isBlockStartPoint(node, offset) |
| 4190 || isBlockEndPoint(node, offset); |
| 4191 } |
| 4192 |
| 4193 function blockExtend(range) { |
| 4194 // "Let start node, start offset, end node, and end offset be the start |
| 4195 // and end nodes and offsets of the range." |
| 4196 var startNode = range.startContainer; |
| 4197 var startOffset = range.startOffset; |
| 4198 var endNode = range.endContainer; |
| 4199 var endOffset = range.endOffset; |
| 4200 |
| 4201 // "If some ancestor container of start node is an li, set start offset to |
| 4202 // the index of the last such li in tree order, and set start node to that |
| 4203 // li's parent." |
| 4204 var liAncestors = getAncestors(startNode).concat(startNode) |
| 4205 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
| 4206 .slice(-1); |
| 4207 if (liAncestors.length) { |
| 4208 startOffset = getNodeIndex(liAncestors[0]); |
| 4209 startNode = liAncestors[0].parentNode; |
| 4210 } |
| 4211 |
| 4212 // "If (start node, start offset) is not a block start point, repeat the |
| 4213 // following steps:" |
| 4214 if (!isBlockStartPoint(startNode, startOffset)) do { |
| 4215 // "If start offset is zero, set it to start node's index, then set |
| 4216 // start node to its parent." |
| 4217 if (startOffset == 0) { |
| 4218 startOffset = getNodeIndex(startNode); |
| 4219 startNode = startNode.parentNode; |
| 4220 |
| 4221 // "Otherwise, subtract one from start offset." |
| 4222 } else { |
| 4223 startOffset--; |
| 4224 } |
| 4225 |
| 4226 // "If (start node, start offset) is a block boundary point, break from |
| 4227 // this loop." |
| 4228 } while (!isBlockBoundaryPoint(startNode, startOffset)); |
| 4229 |
| 4230 // "While start offset is zero and start node's parent is not null, set |
| 4231 // start offset to start node's index, then set start node to its parent." |
| 4232 while (startOffset == 0 |
| 4233 && startNode.parentNode) { |
| 4234 startOffset = getNodeIndex(startNode); |
| 4235 startNode = startNode.parentNode; |
| 4236 } |
| 4237 |
| 4238 // "If some ancestor container of end node is an li, set end offset to one |
| 4239 // plus the index of the last such li in tree order, and set end node to |
| 4240 // that li's parent." |
| 4241 var liAncestors = getAncestors(endNode).concat(endNode) |
| 4242 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
| 4243 .slice(-1); |
| 4244 if (liAncestors.length) { |
| 4245 endOffset = 1 + getNodeIndex(liAncestors[0]); |
| 4246 endNode = liAncestors[0].parentNode; |
| 4247 } |
| 4248 |
| 4249 // "If (end node, end offset) is not a block end point, repeat the |
| 4250 // following steps:" |
| 4251 if (!isBlockEndPoint(endNode, endOffset)) do { |
| 4252 // "If end offset is end node's length, set it to one plus end node's |
| 4253 // index, then set end node to its parent." |
| 4254 if (endOffset == getNodeLength(endNode)) { |
| 4255 endOffset = 1 + getNodeIndex(endNode); |
| 4256 endNode = endNode.parentNode; |
| 4257 |
| 4258 // "Otherwise, add one to end offset. |
| 4259 } else { |
| 4260 endOffset++; |
| 4261 } |
| 4262 |
| 4263 // "If (end node, end offset) is a block boundary point, break from |
| 4264 // this loop." |
| 4265 } while (!isBlockBoundaryPoint(endNode, endOffset)); |
| 4266 |
| 4267 // "While end offset is end node's length and end node's parent is not |
| 4268 // null, set end offset to one plus end node's index, then set end node to |
| 4269 // its parent." |
| 4270 while (endOffset == getNodeLength(endNode) |
| 4271 && endNode.parentNode) { |
| 4272 endOffset = 1 + getNodeIndex(endNode); |
| 4273 endNode = endNode.parentNode; |
| 4274 } |
| 4275 |
| 4276 // "Let new range be a new range whose start and end nodes and offsets |
| 4277 // are start node, start offset, end node, and end offset." |
| 4278 var newRange = startNode.ownerDocument.createRange(); |
| 4279 newRange.setStart(startNode, startOffset); |
| 4280 newRange.setEnd(endNode, endOffset); |
| 4281 |
| 4282 // "Return new range." |
| 4283 return newRange; |
| 4284 } |
| 4285 |
| 4286 function followsLineBreak(node) { |
| 4287 // "Let offset be zero." |
| 4288 var offset = 0; |
| 4289 |
| 4290 // "While (node, offset) is not a block boundary point:" |
| 4291 while (!isBlockBoundaryPoint(node, offset)) { |
| 4292 // "If node has a visible child with index offset minus one, return |
| 4293 // false." |
| 4294 if (0 <= offset - 1 |
| 4295 && offset - 1 < node.childNodes.length |
| 4296 && isVisible(node.childNodes[offset - 1])) { |
| 4297 return false; |
| 4298 } |
| 4299 |
| 4300 // "If offset is zero or node has no children, set offset to node's |
| 4301 // index, then set node to its parent." |
| 4302 if (offset == 0 |
| 4303 || !node.hasChildNodes()) { |
| 4304 offset = getNodeIndex(node); |
| 4305 node = node.parentNode; |
| 4306 |
| 4307 // "Otherwise, set node to its child with index offset minus one, then |
| 4308 // set offset to node's length." |
| 4309 } else { |
| 4310 node = node.childNodes[offset - 1]; |
| 4311 offset = getNodeLength(node); |
| 4312 } |
| 4313 } |
| 4314 |
| 4315 // "Return true." |
| 4316 return true; |
| 4317 } |
| 4318 |
| 4319 function precedesLineBreak(node) { |
| 4320 // "Let offset be node's length." |
| 4321 var offset = getNodeLength(node); |
| 4322 |
| 4323 // "While (node, offset) is not a block boundary point:" |
| 4324 while (!isBlockBoundaryPoint(node, offset)) { |
| 4325 // "If node has a visible child with index offset, return false." |
| 4326 if (offset < node.childNodes.length |
| 4327 && isVisible(node.childNodes[offset])) { |
| 4328 return false; |
| 4329 } |
| 4330 |
| 4331 // "If offset is node's length or node has no children, set offset to |
| 4332 // one plus node's index, then set node to its parent." |
| 4333 if (offset == getNodeLength(node) |
| 4334 || !node.hasChildNodes()) { |
| 4335 offset = 1 + getNodeIndex(node); |
| 4336 node = node.parentNode; |
| 4337 |
| 4338 // "Otherwise, set node to its child with index offset and set offset |
| 4339 // to zero." |
| 4340 } else { |
| 4341 node = node.childNodes[offset]; |
| 4342 offset = 0; |
| 4343 } |
| 4344 } |
| 4345 |
| 4346 // "Return true." |
| 4347 return true; |
| 4348 } |
| 4349 |
| 4350 //@} |
| 4351 ///// Recording and restoring overrides ///// |
| 4352 //@{ |
| 4353 |
| 4354 function recordCurrentOverrides() { |
| 4355 // "Let overrides be a list of (string, string or boolean) ordered pairs, |
| 4356 // initially empty." |
| 4357 var overrides = []; |
| 4358 |
| 4359 // "If there is a value override for "createLink", add ("createLink", value |
| 4360 // override for "createLink") to overrides." |
| 4361 if (getValueOverride("createlink") !== undefined) { |
| 4362 overrides.push(["createlink", getValueOverride("createlink")]); |
| 4363 } |
| 4364 |
| 4365 // "For each command in the list "bold", "italic", "strikethrough", |
| 4366 // "subscript", "superscript", "underline", in order: if there is a state |
| 4367 // override for command, add (command, command's state override) to |
| 4368 // overrides." |
| 4369 ["bold", "italic", "strikethrough", "subscript", "superscript", |
| 4370 "underline"].forEach(function(command) { |
| 4371 if (getStateOverride(command) !== undefined) { |
| 4372 overrides.push([command, getStateOverride(command)]); |
| 4373 } |
| 4374 }); |
| 4375 |
| 4376 // "For each command in the list "fontName", "fontSize", "foreColor", |
| 4377 // "hiliteColor", in order: if there is a value override for command, add |
| 4378 // (command, command's value override) to overrides." |
| 4379 ["fontname", "fontsize", "forecolor", |
| 4380 "hilitecolor"].forEach(function(command) { |
| 4381 if (getValueOverride(command) !== undefined) { |
| 4382 overrides.push([command, getValueOverride(command)]); |
| 4383 } |
| 4384 }); |
| 4385 |
| 4386 // "Return overrides." |
| 4387 return overrides; |
| 4388 } |
| 4389 |
| 4390 function recordCurrentStatesAndValues() { |
| 4391 // "Let overrides be a list of (string, string or boolean) ordered pairs, |
| 4392 // initially empty." |
| 4393 var overrides = []; |
| 4394 |
| 4395 // "Let node be the first formattable node effectively contained in the |
| 4396 // active range, or null if there is none." |
| 4397 var node = getAllEffectivelyContainedNodes(getActiveRange()) |
| 4398 .filter(isFormattableNode)[0]; |
| 4399 |
| 4400 // "If node is null, return overrides." |
| 4401 if (!node) { |
| 4402 return overrides; |
| 4403 } |
| 4404 |
| 4405 // "Add ("createLink", node's effective command value for "createLink") to |
| 4406 // overrides." |
| 4407 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")])
; |
| 4408 |
| 4409 // "For each command in the list "bold", "italic", "strikethrough", |
| 4410 // "subscript", "superscript", "underline", in order: if node's effective |
| 4411 // command value for command is one of its inline command activated values, |
| 4412 // add (command, true) to overrides, and otherwise add (command, false) to |
| 4413 // overrides." |
| 4414 ["bold", "italic", "strikethrough", "subscript", "superscript", |
| 4415 "underline"].forEach(function(command) { |
| 4416 if (commands[command].inlineCommandActivatedValues |
| 4417 .indexOf(getEffectiveCommandValue(node, command)) != -1) { |
| 4418 overrides.push([command, true]); |
| 4419 } else { |
| 4420 overrides.push([command, false]); |
| 4421 } |
| 4422 }); |
| 4423 |
| 4424 // "For each command in the list "fontName", "foreColor", "hiliteColor", in |
| 4425 // order: add (command, command's value) to overrides." |
| 4426 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(comman
d) { |
| 4427 overrides.push([command, commands[command].value()]); |
| 4428 }); |
| 4429 |
| 4430 // "Add ("fontSize", node's effective command value for "fontSize") to |
| 4431 // overrides." |
| 4432 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); |
| 4433 |
| 4434 // "Return overrides." |
| 4435 return overrides; |
| 4436 } |
| 4437 |
| 4438 function restoreStatesAndValues(overrides) { |
| 4439 // "Let node be the first formattable node effectively contained in the |
| 4440 // active range, or null if there is none." |
| 4441 var node = getAllEffectivelyContainedNodes(getActiveRange()) |
| 4442 .filter(isFormattableNode)[0]; |
| 4443 |
| 4444 // "If node is not null, then for each (command, override) pair in |
| 4445 // overrides, in order:" |
| 4446 if (node) { |
| 4447 for (var i = 0; i < overrides.length; i++) { |
| 4448 var command = overrides[i][0]; |
| 4449 var override = overrides[i][1]; |
| 4450 |
| 4451 // "If override is a boolean, and queryCommandState(command) |
| 4452 // returns something different from override, take the action for |
| 4453 // command, with value equal to the empty string." |
| 4454 if (typeof override == "boolean" |
| 4455 && myQueryCommandState(command) != override) { |
| 4456 commands[command].action(""); |
| 4457 |
| 4458 // "Otherwise, if override is a string, and command is neither |
| 4459 // "createLink" nor "fontSize", and queryCommandValue(command) |
| 4460 // returns something not equivalent to override, take the action |
| 4461 // for command, with value equal to override." |
| 4462 } else if (typeof override == "string" |
| 4463 && command != "createlink" |
| 4464 && command != "fontsize" |
| 4465 && !areEquivalentValues(command, myQueryCommandValue(command), overr
ide)) { |
| 4466 commands[command].action(override); |
| 4467 |
| 4468 // "Otherwise, if override is a string; and command is |
| 4469 // "createLink"; and either there is a value override for |
| 4470 // "createLink" that is not equal to override, or there is no value |
| 4471 // override for "createLink" and node's effective command value for |
| 4472 // "createLink" is not equal to override: take the action for |
| 4473 // "createLink", with value equal to override." |
| 4474 } else if (typeof override == "string" |
| 4475 && command == "createlink" |
| 4476 && ( |
| 4477 ( |
| 4478 getValueOverride("createlink") !== undefined |
| 4479 && getValueOverride("createlink") !== override |
| 4480 ) || ( |
| 4481 getValueOverride("createlink") === undefined |
| 4482 && getEffectiveCommandValue(node, "createlink") !== override |
| 4483 ) |
| 4484 )) { |
| 4485 commands.createlink.action(override); |
| 4486 |
| 4487 // "Otherwise, if override is a string; and command is "fontSize"; |
| 4488 // and either there is a value override for "fontSize" that is not |
| 4489 // equal to override, or there is no value override for "fontSize" |
| 4490 // and node's effective command value for "fontSize" is not loosely |
| 4491 // equivalent to override:" |
| 4492 } else if (typeof override == "string" |
| 4493 && command == "fontsize" |
| 4494 && ( |
| 4495 ( |
| 4496 getValueOverride("fontsize") !== undefined |
| 4497 && getValueOverride("fontsize") !== override |
| 4498 ) || ( |
| 4499 getValueOverride("fontsize") === undefined |
| 4500 && !areLooselyEquivalentValues(command, getEffectiveCommandV
alue(node, "fontsize"), override) |
| 4501 ) |
| 4502 )) { |
| 4503 // "Convert override to an integer number of pixels, and set |
| 4504 // override to the legacy font size for the result." |
| 4505 override = getLegacyFontSize(override); |
| 4506 |
| 4507 // "Take the action for "fontSize", with value equal to |
| 4508 // override." |
| 4509 commands.fontsize.action(override); |
| 4510 |
| 4511 // "Otherwise, continue this loop from the beginning." |
| 4512 } else { |
| 4513 continue; |
| 4514 } |
| 4515 |
| 4516 // "Set node to the first formattable node effectively contained in |
| 4517 // the active range, if there is one." |
| 4518 node = getAllEffectivelyContainedNodes(getActiveRange()) |
| 4519 .filter(isFormattableNode)[0] |
| 4520 || node; |
| 4521 } |
| 4522 |
| 4523 // "Otherwise, for each (command, override) pair in overrides, in order:" |
| 4524 } else { |
| 4525 for (var i = 0; i < overrides.length; i++) { |
| 4526 var command = overrides[i][0]; |
| 4527 var override = overrides[i][1]; |
| 4528 |
| 4529 // "If override is a boolean, set the state override for command to |
| 4530 // override." |
| 4531 if (typeof override == "boolean") { |
| 4532 setStateOverride(command, override); |
| 4533 } |
| 4534 |
| 4535 // "If override is a string, set the value override for command to |
| 4536 // override." |
| 4537 if (typeof override == "string") { |
| 4538 setValueOverride(command, override); |
| 4539 } |
| 4540 } |
| 4541 } |
| 4542 } |
| 4543 |
| 4544 //@} |
| 4545 ///// Deleting the selection ///// |
| 4546 //@{ |
| 4547 |
| 4548 // The flags argument is a dictionary that can have blockMerging, |
| 4549 // stripWrappers, and/or direction as keys. |
| 4550 function deleteSelection(flags) { |
| 4551 if (flags === undefined) { |
| 4552 flags = {}; |
| 4553 } |
| 4554 |
| 4555 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : t
rue; |
| 4556 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers)
: true; |
| 4557 var direction = "direction" in flags ? flags.direction : "forward"; |
| 4558 |
| 4559 // "If the active range is null, abort these steps and do nothing." |
| 4560 if (!getActiveRange()) { |
| 4561 return; |
| 4562 } |
| 4563 |
| 4564 // "Canonicalize whitespace at the active range's start." |
| 4565 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().sta
rtOffset); |
| 4566 |
| 4567 // "Canonicalize whitespace at the active range's end." |
| 4568 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOf
fset); |
| 4569 |
| 4570 // "Let (start node, start offset) be the last equivalent point for the |
| 4571 // active range's start." |
| 4572 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiv
eRange().startOffset); |
| 4573 var startNode = start[0]; |
| 4574 var startOffset = start[1]; |
| 4575 |
| 4576 // "Let (end node, end offset) be the first equivalent point for the active |
| 4577 // range's end." |
| 4578 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRa
nge().endOffset); |
| 4579 var endNode = end[0]; |
| 4580 var endOffset = end[1]; |
| 4581 |
| 4582 // "If (end node, end offset) is not after (start node, start offset):" |
| 4583 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { |
| 4584 // "If direction is "forward", call collapseToStart() on the context |
| 4585 // object's Selection." |
| 4586 // |
| 4587 // Here and in a few other places, we check rangeCount to work around a |
| 4588 // WebKit bug: it will sometimes incorrectly remove ranges from the |
| 4589 // selection if nodes are removed, so collapseToStart() will throw. |
| 4590 // This will break everything if we're using an actual selection, but |
| 4591 // if getActiveRange() is really just returning globalRange and that's |
| 4592 // all we care about, it will work fine. I only add the extra check |
| 4593 // for errors I actually hit in testing. |
| 4594 if (direction == "forward") { |
| 4595 if (getSelection().rangeCount) { |
| 4596 getSelection().collapseToStart(); |
| 4597 } |
| 4598 getActiveRange().collapse(true); |
| 4599 |
| 4600 // "Otherwise, call collapseToEnd() on the context object's Selection." |
| 4601 } else { |
| 4602 getSelection().collapseToEnd(); |
| 4603 getActiveRange().collapse(false); |
| 4604 } |
| 4605 |
| 4606 // "Abort these steps." |
| 4607 return; |
| 4608 } |
| 4609 |
| 4610 // "If start node is a Text node and start offset is 0, set start offset to |
| 4611 // the index of start node, then set start node to its parent." |
| 4612 if (startNode.nodeType == Node.TEXT_NODE |
| 4613 && startOffset == 0) { |
| 4614 startOffset = getNodeIndex(startNode); |
| 4615 startNode = startNode.parentNode; |
| 4616 } |
| 4617 |
| 4618 // "If end node is a Text node and end offset is its length, set end offset |
| 4619 // to one plus the index of end node, then set end node to its parent." |
| 4620 if (endNode.nodeType == Node.TEXT_NODE |
| 4621 && endOffset == getNodeLength(endNode)) { |
| 4622 endOffset = 1 + getNodeIndex(endNode); |
| 4623 endNode = endNode.parentNode; |
| 4624 } |
| 4625 |
| 4626 // "Call collapse(start node, start offset) on the context object's |
| 4627 // Selection." |
| 4628 getSelection().collapse(startNode, startOffset); |
| 4629 getActiveRange().setStart(startNode, startOffset); |
| 4630 |
| 4631 // "Call extend(end node, end offset) on the context object's Selection." |
| 4632 getSelection().extend(endNode, endOffset); |
| 4633 getActiveRange().setEnd(endNode, endOffset); |
| 4634 |
| 4635 // "Let start block be the active range's start node." |
| 4636 var startBlock = getActiveRange().startContainer; |
| 4637 |
| 4638 // "While start block's parent is in the same editing host and start block |
| 4639 // is an inline node, set start block to its parent." |
| 4640 while (inSameEditingHost(startBlock, startBlock.parentNode) |
| 4641 && isInlineNode(startBlock)) { |
| 4642 startBlock = startBlock.parentNode; |
| 4643 } |
| 4644 |
| 4645 // "If start block is neither a block node nor an editing host, or "span" |
| 4646 // is not an allowed child of start block, or start block is a td or th, |
| 4647 // set start block to null." |
| 4648 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) |
| 4649 || !isAllowedChild("span", startBlock) |
| 4650 || isHtmlElement(startBlock, ["td", "th"])) { |
| 4651 startBlock = null; |
| 4652 } |
| 4653 |
| 4654 // "Let end block be the active range's end node." |
| 4655 var endBlock = getActiveRange().endContainer; |
| 4656 |
| 4657 // "While end block's parent is in the same editing host and end block is |
| 4658 // an inline node, set end block to its parent." |
| 4659 while (inSameEditingHost(endBlock, endBlock.parentNode) |
| 4660 && isInlineNode(endBlock)) { |
| 4661 endBlock = endBlock.parentNode; |
| 4662 } |
| 4663 |
| 4664 // "If end block is neither a block node nor an editing host, or "span" is |
| 4665 // not an allowed child of end block, or end block is a td or th, set end |
| 4666 // block to null." |
| 4667 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) |
| 4668 || !isAllowedChild("span", endBlock) |
| 4669 || isHtmlElement(endBlock, ["td", "th"])) { |
| 4670 endBlock = null; |
| 4671 } |
| 4672 |
| 4673 // "Record current states and values, and let overrides be the result." |
| 4674 var overrides = recordCurrentStatesAndValues(); |
| 4675 |
| 4676 // "If start node and end node are the same, and start node is an editable |
| 4677 // Text node:" |
| 4678 if (startNode == endNode |
| 4679 && isEditable(startNode) |
| 4680 && startNode.nodeType == Node.TEXT_NODE) { |
| 4681 // "Call deleteData(start offset, end offset − start offset) on start |
| 4682 // node." |
| 4683 startNode.deleteData(startOffset, endOffset - startOffset); |
| 4684 |
| 4685 // "Canonicalize whitespace at (start node, start offset), with fix |
| 4686 // collapsed space false." |
| 4687 canonicalizeWhitespace(startNode, startOffset, false); |
| 4688 |
| 4689 // "If direction is "forward", call collapseToStart() on the context |
| 4690 // object's Selection." |
| 4691 if (direction == "forward") { |
| 4692 if (getSelection().rangeCount) { |
| 4693 getSelection().collapseToStart(); |
| 4694 } |
| 4695 getActiveRange().collapse(true); |
| 4696 |
| 4697 // "Otherwise, call collapseToEnd() on the context object's Selection." |
| 4698 } else { |
| 4699 getSelection().collapseToEnd(); |
| 4700 getActiveRange().collapse(false); |
| 4701 } |
| 4702 |
| 4703 // "Restore states and values from overrides." |
| 4704 restoreStatesAndValues(overrides); |
| 4705 |
| 4706 // "Abort these steps." |
| 4707 return; |
| 4708 } |
| 4709 |
| 4710 // "If start node is an editable Text node, call deleteData() on it, with |
| 4711 // start offset as the first argument and (length of start node − start |
| 4712 // offset) as the second argument." |
| 4713 if (isEditable(startNode) |
| 4714 && startNode.nodeType == Node.TEXT_NODE) { |
| 4715 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset
); |
| 4716 } |
| 4717 |
| 4718 // "Let node list be a list of nodes, initially empty." |
| 4719 // |
| 4720 // "For each node contained in the active range, append node to node list |
| 4721 // if the last member of node list (if any) is not an ancestor of node; |
| 4722 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td." |
| 4723 var nodeList = getContainedNodes(getActiveRange(), |
| 4724 function(node) { |
| 4725 return isEditable(node) |
| 4726 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th",
"td"]); |
| 4727 } |
| 4728 ); |
| 4729 |
| 4730 // "For each node in node list:" |
| 4731 for (var i = 0; i < nodeList.length; i++) { |
| 4732 var node = nodeList[i]; |
| 4733 |
| 4734 // "Let parent be the parent of node." |
| 4735 var parent_ = node.parentNode; |
| 4736 |
| 4737 // "Remove node from parent." |
| 4738 parent_.removeChild(node); |
| 4739 |
| 4740 // "If the block node of parent has no visible children, and parent is |
| 4741 // editable or an editing host, call createElement("br") on the context |
| 4742 // object and append the result as the last child of parent." |
| 4743 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible) |
| 4744 && (isEditable(parent_) || isEditingHost(parent_))) { |
| 4745 parent_.appendChild(document.createElement("br")); |
| 4746 } |
| 4747 |
| 4748 // "If strip wrappers is true or parent is not an ancestor container of |
| 4749 // start node, while parent is an editable inline node with length 0, |
| 4750 // let grandparent be the parent of parent, then remove parent from |
| 4751 // grandparent, then set parent to grandparent." |
| 4752 if (stripWrappers |
| 4753 || (!isAncestor(parent_, startNode) && parent_ != startNode)) { |
| 4754 while (isEditable(parent_) |
| 4755 && isInlineNode(parent_) |
| 4756 && getNodeLength(parent_) == 0) { |
| 4757 var grandparent = parent_.parentNode; |
| 4758 grandparent.removeChild(parent_); |
| 4759 parent_ = grandparent; |
| 4760 } |
| 4761 } |
| 4762 } |
| 4763 |
| 4764 // "If end node is an editable Text node, call deleteData(0, end offset) on |
| 4765 // it." |
| 4766 if (isEditable(endNode) |
| 4767 && endNode.nodeType == Node.TEXT_NODE) { |
| 4768 endNode.deleteData(0, endOffset); |
| 4769 } |
| 4770 |
| 4771 // "Canonicalize whitespace at the active range's start, with fix collapsed |
| 4772 // space false." |
| 4773 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().sta
rtOffset, false); |
| 4774 |
| 4775 // "Canonicalize whitespace at the active range's end, with fix collapsed |
| 4776 // space false." |
| 4777 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOf
fset, false); |
| 4778 |
| 4779 // "If block merging is false, or start block or end block is null, or |
| 4780 // start block is not in the same editing host as end block, or start block |
| 4781 // and end block are the same:" |
| 4782 if (!blockMerging |
| 4783 || !startBlock |
| 4784 || !endBlock |
| 4785 || !inSameEditingHost(startBlock, endBlock) |
| 4786 || startBlock == endBlock) { |
| 4787 // "If direction is "forward", call collapseToStart() on the context |
| 4788 // object's Selection." |
| 4789 if (direction == "forward") { |
| 4790 if (getSelection().rangeCount) { |
| 4791 getSelection().collapseToStart(); |
| 4792 } |
| 4793 getActiveRange().collapse(true); |
| 4794 |
| 4795 // "Otherwise, call collapseToEnd() on the context object's Selection." |
| 4796 } else { |
| 4797 if (getSelection().rangeCount) { |
| 4798 getSelection().collapseToEnd(); |
| 4799 } |
| 4800 getActiveRange().collapse(false); |
| 4801 } |
| 4802 |
| 4803 // "Restore states and values from overrides." |
| 4804 restoreStatesAndValues(overrides); |
| 4805 |
| 4806 // "Abort these steps." |
| 4807 return; |
| 4808 } |
| 4809 |
| 4810 // "If start block has one child, which is a collapsed block prop, remove |
| 4811 // its child from it." |
| 4812 if (startBlock.children.length == 1 |
| 4813 && isCollapsedBlockProp(startBlock.firstChild)) { |
| 4814 startBlock.removeChild(startBlock.firstChild); |
| 4815 } |
| 4816 |
| 4817 // "If start block is an ancestor of end block:" |
| 4818 if (isAncestor(startBlock, endBlock)) { |
| 4819 // "Let reference node be end block." |
| 4820 var referenceNode = endBlock; |
| 4821 |
| 4822 // "While reference node is not a child of start block, set reference |
| 4823 // node to its parent." |
| 4824 while (referenceNode.parentNode != startBlock) { |
| 4825 referenceNode = referenceNode.parentNode; |
| 4826 } |
| 4827 |
| 4828 // "Call collapse() on the context object's Selection, with first |
| 4829 // argument start block and second argument the index of reference |
| 4830 // node." |
| 4831 getSelection().collapse(startBlock, getNodeIndex(referenceNode)); |
| 4832 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode)); |
| 4833 getActiveRange().collapse(true); |
| 4834 |
| 4835 // "If end block has no children:" |
| 4836 if (!endBlock.hasChildNodes()) { |
| 4837 // "While end block is editable and is the only child of its parent |
| 4838 // and is not a child of start block, let parent equal end block, |
| 4839 // then remove end block from parent, then set end block to |
| 4840 // parent." |
| 4841 while (isEditable(endBlock) |
| 4842 && endBlock.parentNode.childNodes.length == 1 |
| 4843 && endBlock.parentNode != startBlock) { |
| 4844 var parent_ = endBlock; |
| 4845 parent_.removeChild(endBlock); |
| 4846 endBlock = parent_; |
| 4847 } |
| 4848 |
| 4849 // "If end block is editable and is not an inline node, and its |
| 4850 // previousSibling and nextSibling are both inline nodes, call |
| 4851 // createElement("br") on the context object and insert it into end |
| 4852 // block's parent immediately after end block." |
| 4853 if (isEditable(endBlock) |
| 4854 && !isInlineNode(endBlock) |
| 4855 && isInlineNode(endBlock.previousSibling) |
| 4856 && isInlineNode(endBlock.nextSibling)) { |
| 4857 endBlock.parentNode.insertBefore(document.createElement("br"), e
ndBlock.nextSibling); |
| 4858 } |
| 4859 |
| 4860 // "If end block is editable, remove it from its parent." |
| 4861 if (isEditable(endBlock)) { |
| 4862 endBlock.parentNode.removeChild(endBlock); |
| 4863 } |
| 4864 |
| 4865 // "Restore states and values from overrides." |
| 4866 restoreStatesAndValues(overrides); |
| 4867 |
| 4868 // "Abort these steps." |
| 4869 return; |
| 4870 } |
| 4871 |
| 4872 // "If end block's firstChild is not an inline node, restore states and |
| 4873 // values from overrides, then abort these steps." |
| 4874 if (!isInlineNode(endBlock.firstChild)) { |
| 4875 restoreStatesAndValues(overrides); |
| 4876 return; |
| 4877 } |
| 4878 |
| 4879 // "Let children be a list of nodes, initially empty." |
| 4880 var children = []; |
| 4881 |
| 4882 // "Append the first child of end block to children." |
| 4883 children.push(endBlock.firstChild); |
| 4884 |
| 4885 // "While children's last member is not a br, and children's last |
| 4886 // member's nextSibling is an inline node, append children's last |
| 4887 // member's nextSibling to children." |
| 4888 while (!isHtmlElement(children[children.length - 1], "br") |
| 4889 && isInlineNode(children[children.length - 1].nextSibling)) { |
| 4890 children.push(children[children.length - 1].nextSibling); |
| 4891 } |
| 4892 |
| 4893 // "Record the values of children, and let values be the result." |
| 4894 var values = recordValues(children); |
| 4895 |
| 4896 // "While children's first member's parent is not start block, split |
| 4897 // the parent of children." |
| 4898 while (children[0].parentNode != startBlock) { |
| 4899 splitParent(children); |
| 4900 } |
| 4901 |
| 4902 // "If children's first member's previousSibling is an editable br, |
| 4903 // remove that br from its parent." |
| 4904 if (isEditable(children[0].previousSibling) |
| 4905 && isHtmlElement(children[0].previousSibling, "br")) { |
| 4906 children[0].parentNode.removeChild(children[0].previousSibling); |
| 4907 } |
| 4908 |
| 4909 // "Otherwise, if start block is a descendant of end block:" |
| 4910 } else if (isDescendant(startBlock, endBlock)) { |
| 4911 // "Call collapse() on the context object's Selection, with first |
| 4912 // argument start block and second argument start block's length." |
| 4913 getSelection().collapse(startBlock, getNodeLength(startBlock)); |
| 4914 getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
| 4915 getActiveRange().collapse(true); |
| 4916 |
| 4917 // "Let reference node be start block." |
| 4918 var referenceNode = startBlock; |
| 4919 |
| 4920 // "While reference node is not a child of end block, set reference |
| 4921 // node to its parent." |
| 4922 while (referenceNode.parentNode != endBlock) { |
| 4923 referenceNode = referenceNode.parentNode; |
| 4924 } |
| 4925 |
| 4926 // "If reference node's nextSibling is an inline node and start block's |
| 4927 // lastChild is a br, remove start block's lastChild from it." |
| 4928 if (isInlineNode(referenceNode.nextSibling) |
| 4929 && isHtmlElement(startBlock.lastChild, "br")) { |
| 4930 startBlock.removeChild(startBlock.lastChild); |
| 4931 } |
| 4932 |
| 4933 // "Let nodes to move be a list of nodes, initially empty." |
| 4934 var nodesToMove = []; |
| 4935 |
| 4936 // "If reference node's nextSibling is neither null nor a block node, |
| 4937 // append it to nodes to move." |
| 4938 if (referenceNode.nextSibling |
| 4939 && !isBlockNode(referenceNode.nextSibling)) { |
| 4940 nodesToMove.push(referenceNode.nextSibling); |
| 4941 } |
| 4942 |
| 4943 // "While nodes to move is nonempty and its last member isn't a br and |
| 4944 // its last member's nextSibling is neither null nor a block node, |
| 4945 // append its last member's nextSibling to nodes to move." |
| 4946 if (nodesToMove.length |
| 4947 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br") |
| 4948 && nodesToMove[nodesToMove.length - 1].nextSibling |
| 4949 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { |
| 4950 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); |
| 4951 } |
| 4952 |
| 4953 // "Record the values of nodes to move, and let values be the result." |
| 4954 var values = recordValues(nodesToMove); |
| 4955 |
| 4956 // "For each node in nodes to move, append node as the last child of |
| 4957 // start block, preserving ranges." |
| 4958 nodesToMove.forEach(function(node) { |
| 4959 movePreservingRanges(node, startBlock, -1); |
| 4960 }); |
| 4961 |
| 4962 // "Otherwise:" |
| 4963 } else { |
| 4964 // "Call collapse() on the context object's Selection, with first |
| 4965 // argument start block and second argument start block's length." |
| 4966 getSelection().collapse(startBlock, getNodeLength(startBlock)); |
| 4967 getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
| 4968 getActiveRange().collapse(true); |
| 4969 |
| 4970 // "If end block's firstChild is an inline node and start block's |
| 4971 // lastChild is a br, remove start block's lastChild from it." |
| 4972 if (isInlineNode(endBlock.firstChild) |
| 4973 && isHtmlElement(startBlock.lastChild, "br")) { |
| 4974 startBlock.removeChild(startBlock.lastChild); |
| 4975 } |
| 4976 |
| 4977 // "Record the values of end block's children, and let values be the |
| 4978 // result." |
| 4979 var values = recordValues([].slice.call(endBlock.childNodes)); |
| 4980 |
| 4981 // "While end block has children, append the first child of end block |
| 4982 // to start block, preserving ranges." |
| 4983 while (endBlock.hasChildNodes()) { |
| 4984 movePreservingRanges(endBlock.firstChild, startBlock, -1); |
| 4985 } |
| 4986 |
| 4987 // "While end block has no children, let parent be the parent of end |
| 4988 // block, then remove end block from parent, then set end block to |
| 4989 // parent." |
| 4990 while (!endBlock.hasChildNodes()) { |
| 4991 var parent_ = endBlock.parentNode; |
| 4992 parent_.removeChild(endBlock); |
| 4993 endBlock = parent_; |
| 4994 } |
| 4995 } |
| 4996 |
| 4997 // "Let ancestor be start block." |
| 4998 var ancestor = startBlock; |
| 4999 |
| 5000 // "While ancestor has an inclusive ancestor ol in the same editing host |
| 5001 // whose nextSibling is also an ol in the same editing host, or an |
| 5002 // inclusive ancestor ul in the same editing host whose nextSibling is also |
| 5003 // a ul in the same editing host:" |
| 5004 while (getInclusiveAncestors(ancestor).some(function(node) { |
| 5005 return inSameEditingHost(ancestor, node) |
| 5006 && ( |
| 5007 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "o
l")) |
| 5008 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling,
"ul")) |
| 5009 ) && inSameEditingHost(ancestor, node.nextSibling); |
| 5010 })) { |
| 5011 // "While ancestor and its nextSibling are not both ols in the same |
| 5012 // editing host, and are also not both uls in the same editing host, |
| 5013 // set ancestor to its parent." |
| 5014 while (!( |
| 5015 isHtmlElement(ancestor, "ol") |
| 5016 && isHtmlElement(ancestor.nextSibling, "ol") |
| 5017 && inSameEditingHost(ancestor, ancestor.nextSibling) |
| 5018 ) && !( |
| 5019 isHtmlElement(ancestor, "ul") |
| 5020 && isHtmlElement(ancestor.nextSibling, "ul") |
| 5021 && inSameEditingHost(ancestor, ancestor.nextSibling) |
| 5022 )) { |
| 5023 ancestor = ancestor.parentNode; |
| 5024 } |
| 5025 |
| 5026 // "While ancestor's nextSibling has children, append ancestor's |
| 5027 // nextSibling's firstChild as the last child of ancestor, preserving |
| 5028 // ranges." |
| 5029 while (ancestor.nextSibling.hasChildNodes()) { |
| 5030 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1); |
| 5031 } |
| 5032 |
| 5033 // "Remove ancestor's nextSibling from its parent." |
| 5034 ancestor.parentNode.removeChild(ancestor.nextSibling); |
| 5035 } |
| 5036 |
| 5037 // "Restore the values from values." |
| 5038 restoreValues(values); |
| 5039 |
| 5040 // "If start block has no children, call createElement("br") on the context |
| 5041 // object and append the result as the last child of start block." |
| 5042 if (!startBlock.hasChildNodes()) { |
| 5043 startBlock.appendChild(document.createElement("br")); |
| 5044 } |
| 5045 |
| 5046 // "Remove extraneous line breaks at the end of start block." |
| 5047 removeExtraneousLineBreaksAtTheEndOf(startBlock); |
| 5048 |
| 5049 // "Restore states and values from overrides." |
| 5050 restoreStatesAndValues(overrides); |
| 5051 } |
| 5052 |
| 5053 |
| 5054 //@} |
| 5055 ///// Splitting a node list's parent ///// |
| 5056 //@{ |
| 5057 |
| 5058 function splitParent(nodeList) { |
| 5059 // "Let original parent be the parent of the first member of node list." |
| 5060 var originalParent = nodeList[0].parentNode; |
| 5061 |
| 5062 // "If original parent is not editable or its parent is null, do nothing |
| 5063 // and abort these steps." |
| 5064 if (!isEditable(originalParent) |
| 5065 || !originalParent.parentNode) { |
| 5066 return; |
| 5067 } |
| 5068 |
| 5069 // "If the first child of original parent is in node list, remove |
| 5070 // extraneous line breaks before original parent." |
| 5071 if (nodeList.indexOf(originalParent.firstChild) != -1) { |
| 5072 removeExtraneousLineBreaksBefore(originalParent); |
| 5073 } |
| 5074 |
| 5075 // "If the first child of original parent is in node list, and original |
| 5076 // parent follows a line break, set follows line break to true. Otherwise, |
| 5077 // set follows line break to false." |
| 5078 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1 |
| 5079 && followsLineBreak(originalParent); |
| 5080 |
| 5081 // "If the last child of original parent is in node list, and original |
| 5082 // parent precedes a line break, set precedes line break to true. |
| 5083 // Otherwise, set precedes line break to false." |
| 5084 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1 |
| 5085 && precedesLineBreak(originalParent); |
| 5086 |
| 5087 // "If the first child of original parent is not in node list, but its last |
| 5088 // child is:" |
| 5089 if (nodeList.indexOf(originalParent.firstChild) == -1 |
| 5090 && nodeList.indexOf(originalParent.lastChild) != -1) { |
| 5091 // "For each node in node list, in reverse order, insert node into the |
| 5092 // parent of original parent immediately after original parent, |
| 5093 // preserving ranges." |
| 5094 for (var i = nodeList.length - 1; i >= 0; i--) { |
| 5095 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + get
NodeIndex(originalParent)); |
| 5096 } |
| 5097 |
| 5098 // "If precedes line break is true, and the last member of node list |
| 5099 // does not precede a line break, call createElement("br") on the |
| 5100 // context object and insert the result immediately after the last |
| 5101 // member of node list." |
| 5102 if (precedesLineBreak_ |
| 5103 && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
| 5104 nodeList[nodeList.length - 1].parentNode.insertBefore(document.creat
eElement("br"), nodeList[nodeList.length - 1].nextSibling); |
| 5105 } |
| 5106 |
| 5107 // "Remove extraneous line breaks at the end of original parent." |
| 5108 removeExtraneousLineBreaksAtTheEndOf(originalParent); |
| 5109 |
| 5110 // "Abort these steps." |
| 5111 return; |
| 5112 } |
| 5113 |
| 5114 // "If the first child of original parent is not in node list:" |
| 5115 if (nodeList.indexOf(originalParent.firstChild) == -1) { |
| 5116 // "Let cloned parent be the result of calling cloneNode(false) on |
| 5117 // original parent." |
| 5118 var clonedParent = originalParent.cloneNode(false); |
| 5119 |
| 5120 // "If original parent has an id attribute, unset it." |
| 5121 originalParent.removeAttribute("id"); |
| 5122 |
| 5123 // "Insert cloned parent into the parent of original parent immediately |
| 5124 // before original parent." |
| 5125 originalParent.parentNode.insertBefore(clonedParent, originalParent); |
| 5126 |
| 5127 // "While the previousSibling of the first member of node list is not |
| 5128 // null, append the first child of original parent as the last child of |
| 5129 // cloned parent, preserving ranges." |
| 5130 while (nodeList[0].previousSibling) { |
| 5131 movePreservingRanges(originalParent.firstChild, clonedParent, cloned
Parent.childNodes.length); |
| 5132 } |
| 5133 } |
| 5134 |
| 5135 // "For each node in node list, insert node into the parent of original |
| 5136 // parent immediately before original parent, preserving ranges." |
| 5137 for (var i = 0; i < nodeList.length; i++) { |
| 5138 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeInde
x(originalParent)); |
| 5139 } |
| 5140 |
| 5141 // "If follows line break is true, and the first member of node list does |
| 5142 // not follow a line break, call createElement("br") on the context object |
| 5143 // and insert the result immediately before the first member of node list." |
| 5144 if (followsLineBreak_ |
| 5145 && !followsLineBreak(nodeList[0])) { |
| 5146 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeLi
st[0]); |
| 5147 } |
| 5148 |
| 5149 // "If the last member of node list is an inline node other than a br, and |
| 5150 // the first child of original parent is a br, and original parent is not |
| 5151 // an inline node, remove the first child of original parent from original |
| 5152 // parent." |
| 5153 if (isInlineNode(nodeList[nodeList.length - 1]) |
| 5154 && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
| 5155 && isHtmlElement(originalParent.firstChild, "br") |
| 5156 && !isInlineNode(originalParent)) { |
| 5157 originalParent.removeChild(originalParent.firstChild); |
| 5158 } |
| 5159 |
| 5160 // "If original parent has no children:" |
| 5161 if (!originalParent.hasChildNodes()) { |
| 5162 // "Remove original parent from its parent." |
| 5163 originalParent.parentNode.removeChild(originalParent); |
| 5164 |
| 5165 // "If precedes line break is true, and the last member of node list |
| 5166 // does not precede a line break, call createElement("br") on the |
| 5167 // context object and insert the result immediately after the last |
| 5168 // member of node list." |
| 5169 if (precedesLineBreak_ |
| 5170 && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
| 5171 nodeList[nodeList.length - 1].parentNode.insertBefore(document.creat
eElement("br"), nodeList[nodeList.length - 1].nextSibling); |
| 5172 } |
| 5173 |
| 5174 // "Otherwise, remove extraneous line breaks before original parent." |
| 5175 } else { |
| 5176 removeExtraneousLineBreaksBefore(originalParent); |
| 5177 } |
| 5178 |
| 5179 // "If node list's last member's nextSibling is null, but its parent is not |
| 5180 // null, remove extraneous line breaks at the end of node list's last |
| 5181 // member's parent." |
| 5182 if (!nodeList[nodeList.length - 1].nextSibling |
| 5183 && nodeList[nodeList.length - 1].parentNode) { |
| 5184 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].paren
tNode); |
| 5185 } |
| 5186 } |
| 5187 |
| 5188 // "To remove a node node while preserving its descendants, split the parent of |
| 5189 // node's children if it has any. If it has no children, instead remove it from |
| 5190 // its parent." |
| 5191 function removePreservingDescendants(node) { |
| 5192 if (node.hasChildNodes()) { |
| 5193 splitParent([].slice.call(node.childNodes)); |
| 5194 } else { |
| 5195 node.parentNode.removeChild(node); |
| 5196 } |
| 5197 } |
| 5198 |
| 5199 |
| 5200 //@} |
| 5201 ///// Canonical space sequences ///// |
| 5202 //@{ |
| 5203 |
| 5204 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { |
| 5205 // "If n is zero, return the empty string." |
| 5206 if (n == 0) { |
| 5207 return ""; |
| 5208 } |
| 5209 |
| 5210 // "If n is one and both non-breaking start and non-breaking end are false, |
| 5211 // return a single space (U+0020)." |
| 5212 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { |
| 5213 return " "; |
| 5214 } |
| 5215 |
| 5216 // "If n is one, return a single non-breaking space (U+00A0)." |
| 5217 if (n == 1) { |
| 5218 return "\xa0"; |
| 5219 } |
| 5220 |
| 5221 // "Let buffer be the empty string." |
| 5222 var buffer = ""; |
| 5223 |
| 5224 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. |
| 5225 // Otherwise, let it be U+0020 U+00A0." |
| 5226 var repeatedPair; |
| 5227 if (nonBreakingStart) { |
| 5228 repeatedPair = "\xa0 "; |
| 5229 } else { |
| 5230 repeatedPair = " \xa0"; |
| 5231 } |
| 5232 |
| 5233 // "While n is greater than three, append repeated pair to buffer and |
| 5234 // subtract two from n." |
| 5235 while (n > 3) { |
| 5236 buffer += repeatedPair; |
| 5237 n -= 2; |
| 5238 } |
| 5239 |
| 5240 // "If n is three, append a three-element string to buffer depending on |
| 5241 // non-breaking start and non-breaking end:" |
| 5242 if (n == 3) { |
| 5243 buffer += |
| 5244 !nonBreakingStart && !nonBreakingEnd ? " \xa0 " |
| 5245 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " |
| 5246 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" |
| 5247 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" |
| 5248 : "impossible"; |
| 5249 |
| 5250 // "Otherwise, append a two-element string to buffer depending on |
| 5251 // non-breaking start and non-breaking end:" |
| 5252 } else { |
| 5253 buffer += |
| 5254 !nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
| 5255 : nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
| 5256 : !nonBreakingStart && nonBreakingEnd ? " \xa0" |
| 5257 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" |
| 5258 : "impossible"; |
| 5259 } |
| 5260 |
| 5261 // "Return buffer." |
| 5262 return buffer; |
| 5263 } |
| 5264 |
| 5265 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) { |
| 5266 if (fixCollapsedSpace === undefined) { |
| 5267 // "an optional boolean argument fix collapsed space that defaults to |
| 5268 // true" |
| 5269 fixCollapsedSpace = true; |
| 5270 } |
| 5271 |
| 5272 // "If node is neither editable nor an editing host, abort these steps." |
| 5273 if (!isEditable(node) && !isEditingHost(node)) { |
| 5274 return; |
| 5275 } |
| 5276 |
| 5277 // "Let start node equal node and let start offset equal offset." |
| 5278 var startNode = node; |
| 5279 var startOffset = offset; |
| 5280 |
| 5281 // "Repeat the following steps:" |
| 5282 while (true) { |
| 5283 // "If start node has a child in the same editing host with index start |
| 5284 // offset minus one, set start node to that child, then set start |
| 5285 // offset to start node's length." |
| 5286 if (0 <= startOffset - 1 |
| 5287 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1]))
{ |
| 5288 startNode = startNode.childNodes[startOffset - 1]; |
| 5289 startOffset = getNodeLength(startNode); |
| 5290 |
| 5291 // "Otherwise, if start offset is zero and start node does not follow a |
| 5292 // line break and start node's parent is in the same editing host, set |
| 5293 // start offset to start node's index, then set start node to its |
| 5294 // parent." |
| 5295 } else if (startOffset == 0 |
| 5296 && !followsLineBreak(startNode) |
| 5297 && inSameEditingHost(startNode, startNode.parentNode)) { |
| 5298 startOffset = getNodeIndex(startNode); |
| 5299 startNode = startNode.parentNode; |
| 5300 |
| 5301 // "Otherwise, if start node is a Text node and its parent's resolved |
| 5302 // value for "white-space" is neither "pre" nor "pre-wrap" and start |
| 5303 // offset is not zero and the (start offset − 1)st element of start |
| 5304 // node's data is a space (0x0020) or non-breaking space (0x00A0), |
| 5305 // subtract one from start offset." |
| 5306 } else if (startNode.nodeType == Node.TEXT_NODE |
| 5307 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).wh
iteSpace) == -1 |
| 5308 && startOffset != 0 |
| 5309 && /[ \xa0]/.test(startNode.data[startOffset - 1])) { |
| 5310 startOffset--; |
| 5311 |
| 5312 // "Otherwise, break from this loop." |
| 5313 } else { |
| 5314 break; |
| 5315 } |
| 5316 } |
| 5317 |
| 5318 // "Let end node equal start node and end offset equal start offset." |
| 5319 var endNode = startNode; |
| 5320 var endOffset = startOffset; |
| 5321 |
| 5322 // "Let length equal zero." |
| 5323 var length = 0; |
| 5324 |
| 5325 // "Let collapse spaces be true if start offset is zero and start node |
| 5326 // follows a line break, otherwise false." |
| 5327 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode); |
| 5328 |
| 5329 // "Repeat the following steps:" |
| 5330 while (true) { |
| 5331 // "If end node has a child in the same editing host with index end |
| 5332 // offset, set end node to that child, then set end offset to zero." |
| 5333 if (endOffset < endNode.childNodes.length |
| 5334 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { |
| 5335 endNode = endNode.childNodes[endOffset]; |
| 5336 endOffset = 0; |
| 5337 |
| 5338 // "Otherwise, if end offset is end node's length and end node does not |
| 5339 // precede a line break and end node's parent is in the same editing |
| 5340 // host, set end offset to one plus end node's index, then set end node |
| 5341 // to its parent." |
| 5342 } else if (endOffset == getNodeLength(endNode) |
| 5343 && !precedesLineBreak(endNode) |
| 5344 && inSameEditingHost(endNode, endNode.parentNode)) { |
| 5345 endOffset = 1 + getNodeIndex(endNode); |
| 5346 endNode = endNode.parentNode; |
| 5347 |
| 5348 // "Otherwise, if end node is a Text node and its parent's resolved |
| 5349 // value for "white-space" is neither "pre" nor "pre-wrap" and end |
| 5350 // offset is not end node's length and the end offsetth element of |
| 5351 // end node's data is a space (0x0020) or non-breaking space (0x00A0):" |
| 5352 } else if (endNode.nodeType == Node.TEXT_NODE |
| 5353 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whit
eSpace) == -1 |
| 5354 && endOffset != getNodeLength(endNode) |
| 5355 && /[ \xa0]/.test(endNode.data[endOffset])) { |
| 5356 // "If fix collapsed space is true, and collapse spaces is true, |
| 5357 // and the end offsetth code unit of end node's data is a space |
| 5358 // (0x0020): call deleteData(end offset, 1) on end node, then |
| 5359 // continue this loop from the beginning." |
| 5360 if (fixCollapsedSpace |
| 5361 && collapseSpaces |
| 5362 && " " == endNode.data[endOffset]) { |
| 5363 endNode.deleteData(endOffset, 1); |
| 5364 continue; |
| 5365 } |
| 5366 |
| 5367 // "Set collapse spaces to true if the end offsetth element of end |
| 5368 // node's data is a space (0x0020), false otherwise." |
| 5369 collapseSpaces = " " == endNode.data[endOffset]; |
| 5370 |
| 5371 // "Add one to end offset." |
| 5372 endOffset++; |
| 5373 |
| 5374 // "Add one to length." |
| 5375 length++; |
| 5376 |
| 5377 // "Otherwise, break from this loop." |
| 5378 } else { |
| 5379 break; |
| 5380 } |
| 5381 } |
| 5382 |
| 5383 // "If fix collapsed space is true, then while (start node, start offset) |
| 5384 // is before (end node, end offset):" |
| 5385 if (fixCollapsedSpace) { |
| 5386 while (getPosition(startNode, startOffset, endNode, endOffset) == "befor
e") { |
| 5387 // "If end node has a child in the same editing host with index end |
| 5388 // offset − 1, set end node to that child, then set end offset to en
d |
| 5389 // node's length." |
| 5390 if (0 <= endOffset - 1 |
| 5391 && endOffset - 1 < endNode.childNodes.length |
| 5392 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) { |
| 5393 endNode = endNode.childNodes[endOffset - 1]; |
| 5394 endOffset = getNodeLength(endNode); |
| 5395 |
| 5396 // "Otherwise, if end offset is zero and end node's parent is in the |
| 5397 // same editing host, set end offset to end node's index, then set e
nd |
| 5398 // node to its parent." |
| 5399 } else if (endOffset == 0 |
| 5400 && inSameEditingHost(endNode, endNode.parentNode)) { |
| 5401 endOffset = getNodeIndex(endNode); |
| 5402 endNode = endNode.parentNode; |
| 5403 |
| 5404 // "Otherwise, if end node is a Text node and its parent's resolved |
| 5405 // value for "white-space" is neither "pre" nor "pre-wrap" and end |
| 5406 // offset is end node's length and the last code unit of end node's |
| 5407 // data is a space (0x0020) and end node precedes a line break:" |
| 5408 } else if (endNode.nodeType == Node.TEXT_NODE |
| 5409 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).
whiteSpace) == -1 |
| 5410 && endOffset == getNodeLength(endNode) |
| 5411 && endNode.data[endNode.data.length - 1] == " " |
| 5412 && precedesLineBreak(endNode)) { |
| 5413 // "Subtract one from end offset." |
| 5414 endOffset--; |
| 5415 |
| 5416 // "Subtract one from length." |
| 5417 length--; |
| 5418 |
| 5419 // "Call deleteData(end offset, 1) on end node." |
| 5420 endNode.deleteData(endOffset, 1); |
| 5421 |
| 5422 // "Otherwise, break from this loop." |
| 5423 } else { |
| 5424 break; |
| 5425 } |
| 5426 } |
| 5427 } |
| 5428 |
| 5429 // "Let replacement whitespace be the canonical space sequence of length |
| 5430 // length. non-breaking start is true if start offset is zero and start |
| 5431 // node follows a line break, and false otherwise. non-breaking end is true |
| 5432 // if end offset is end node's length and end node precedes a line break, |
| 5433 // and false otherwise." |
| 5434 var replacementWhitespace = canonicalSpaceSequence(length, |
| 5435 startOffset == 0 && followsLineBreak(startNode), |
| 5436 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); |
| 5437 |
| 5438 // "While (start node, start offset) is before (end node, end offset):" |
| 5439 while (getPosition(startNode, startOffset, endNode, endOffset) == "before")
{ |
| 5440 // "If start node has a child with index start offset, set start node |
| 5441 // to that child, then set start offset to zero." |
| 5442 if (startOffset < startNode.childNodes.length) { |
| 5443 startNode = startNode.childNodes[startOffset]; |
| 5444 startOffset = 0; |
| 5445 |
| 5446 // "Otherwise, if start node is not a Text node or if start offset is |
| 5447 // start node's length, set start offset to one plus start node's |
| 5448 // index, then set start node to its parent." |
| 5449 } else if (startNode.nodeType != Node.TEXT_NODE |
| 5450 || startOffset == getNodeLength(startNode)) { |
| 5451 startOffset = 1 + getNodeIndex(startNode); |
| 5452 startNode = startNode.parentNode; |
| 5453 |
| 5454 // "Otherwise:" |
| 5455 } else { |
| 5456 // "Remove the first element from replacement whitespace, and let |
| 5457 // element be that element." |
| 5458 var element = replacementWhitespace[0]; |
| 5459 replacementWhitespace = replacementWhitespace.slice(1); |
| 5460 |
| 5461 // "If element is not the same as the start offsetth element of |
| 5462 // start node's data:" |
| 5463 if (element != startNode.data[startOffset]) { |
| 5464 // "Call insertData(start offset, element) on start node." |
| 5465 startNode.insertData(startOffset, element); |
| 5466 |
| 5467 // "Call deleteData(start offset + 1, 1) on start node." |
| 5468 startNode.deleteData(startOffset + 1, 1); |
| 5469 } |
| 5470 |
| 5471 // "Add one to start offset." |
| 5472 startOffset++; |
| 5473 } |
| 5474 } |
| 5475 } |
| 5476 |
| 5477 |
| 5478 //@} |
| 5479 ///// Indenting and outdenting ///// |
| 5480 //@{ |
| 5481 |
| 5482 function indentNodes(nodeList) { |
| 5483 // "If node list is empty, do nothing and abort these steps." |
| 5484 if (!nodeList.length) { |
| 5485 return; |
| 5486 } |
| 5487 |
| 5488 // "Let first node be the first member of node list." |
| 5489 var firstNode = nodeList[0]; |
| 5490 |
| 5491 // "If first node's parent is an ol or ul:" |
| 5492 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) { |
| 5493 // "Let tag be the local name of the parent of first node." |
| 5494 var tag = firstNode.parentNode.tagName; |
| 5495 |
| 5496 // "Wrap node list, with sibling criteria returning true for an HTML |
| 5497 // element with local name tag and false otherwise, and new parent |
| 5498 // instructions returning the result of calling createElement(tag) on |
| 5499 // the ownerDocument of first node." |
| 5500 wrap(nodeList, |
| 5501 function(node) { return isHtmlElement(node, tag) }, |
| 5502 function() { return firstNode.ownerDocument.createElement(tag) }); |
| 5503 |
| 5504 // "Abort these steps." |
| 5505 return; |
| 5506 } |
| 5507 |
| 5508 // "Wrap node list, with sibling criteria returning true for a simple |
| 5509 // indentation element and false otherwise, and new parent instructions |
| 5510 // returning the result of calling createElement("blockquote") on the |
| 5511 // ownerDocument of first node. Let new parent be the result." |
| 5512 var newParent = wrap(nodeList, |
| 5513 function(node) { return isSimpleIndentationElement(node) }, |
| 5514 function() { return firstNode.ownerDocument.createElement("blockquote")
}); |
| 5515 |
| 5516 // "Fix disallowed ancestors of new parent." |
| 5517 fixDisallowedAncestors(newParent); |
| 5518 } |
| 5519 |
| 5520 function outdentNode(node) { |
| 5521 // "If node is not editable, abort these steps." |
| 5522 if (!isEditable(node)) { |
| 5523 return; |
| 5524 } |
| 5525 |
| 5526 // "If node is a simple indentation element, remove node, preserving its |
| 5527 // descendants. Then abort these steps." |
| 5528 if (isSimpleIndentationElement(node)) { |
| 5529 removePreservingDescendants(node); |
| 5530 return; |
| 5531 } |
| 5532 |
| 5533 // "If node is an indentation element:" |
| 5534 if (isIndentationElement(node)) { |
| 5535 // "Unset the dir attribute of node, if any." |
| 5536 node.removeAttribute("dir"); |
| 5537 |
| 5538 // "Unset the margin, padding, and border CSS properties of node." |
| 5539 node.style.margin = ""; |
| 5540 node.style.padding = ""; |
| 5541 node.style.border = ""; |
| 5542 if (node.getAttribute("style") == "" |
| 5543 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551 |
| 5544 || node.getAttribute("style") == "border-width: initial; border-color: i
nitial; ") { |
| 5545 node.removeAttribute("style"); |
| 5546 } |
| 5547 |
| 5548 // "Set the tag name of node to "div"." |
| 5549 setTagName(node, "div"); |
| 5550 |
| 5551 // "Abort these steps." |
| 5552 return; |
| 5553 } |
| 5554 |
| 5555 // "Let current ancestor be node's parent." |
| 5556 var currentAncestor = node.parentNode; |
| 5557 |
| 5558 // "Let ancestor list be a list of nodes, initially empty." |
| 5559 var ancestorList = []; |
| 5560 |
| 5561 // "While current ancestor is an editable Element that is neither a simple |
| 5562 // indentation element nor an ol nor a ul, append current ancestor to |
| 5563 // ancestor list and then set current ancestor to its parent." |
| 5564 while (isEditable(currentAncestor) |
| 5565 && currentAncestor.nodeType == Node.ELEMENT_NODE |
| 5566 && !isSimpleIndentationElement(currentAncestor) |
| 5567 && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
| 5568 ancestorList.push(currentAncestor); |
| 5569 currentAncestor = currentAncestor.parentNode; |
| 5570 } |
| 5571 |
| 5572 // "If current ancestor is not an editable simple indentation element:" |
| 5573 if (!isEditable(currentAncestor) |
| 5574 || !isSimpleIndentationElement(currentAncestor)) { |
| 5575 // "Let current ancestor be node's parent." |
| 5576 currentAncestor = node.parentNode; |
| 5577 |
| 5578 // "Let ancestor list be the empty list." |
| 5579 ancestorList = []; |
| 5580 |
| 5581 // "While current ancestor is an editable Element that is neither an |
| 5582 // indentation element nor an ol nor a ul, append current ancestor to |
| 5583 // ancestor list and then set current ancestor to its parent." |
| 5584 while (isEditable(currentAncestor) |
| 5585 && currentAncestor.nodeType == Node.ELEMENT_NODE |
| 5586 && !isIndentationElement(currentAncestor) |
| 5587 && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
| 5588 ancestorList.push(currentAncestor); |
| 5589 currentAncestor = currentAncestor.parentNode; |
| 5590 } |
| 5591 } |
| 5592 |
| 5593 // "If node is an ol or ul and current ancestor is not an editable |
| 5594 // indentation element:" |
| 5595 if (isHtmlElement(node, ["OL", "UL"]) |
| 5596 && (!isEditable(currentAncestor) |
| 5597 || !isIndentationElement(currentAncestor))) { |
| 5598 // "Unset the reversed, start, and type attributes of node, if any are |
| 5599 // set." |
| 5600 node.removeAttribute("reversed"); |
| 5601 node.removeAttribute("start"); |
| 5602 node.removeAttribute("type"); |
| 5603 |
| 5604 // "Let children be the children of node." |
| 5605 var children = [].slice.call(node.childNodes); |
| 5606 |
| 5607 // "If node has attributes, and its parent is not an ol or ul, set the |
| 5608 // tag name of node to "div"." |
| 5609 if (node.attributes.length |
| 5610 && !isHtmlElement(node.parentNode, ["OL", "UL"])) { |
| 5611 setTagName(node, "div"); |
| 5612 |
| 5613 // "Otherwise:" |
| 5614 } else { |
| 5615 // "Record the values of node's children, and let values be the |
| 5616 // result." |
| 5617 var values = recordValues([].slice.call(node.childNodes)); |
| 5618 |
| 5619 // "Remove node, preserving its descendants." |
| 5620 removePreservingDescendants(node); |
| 5621 |
| 5622 // "Restore the values from values." |
| 5623 restoreValues(values); |
| 5624 } |
| 5625 |
| 5626 // "Fix disallowed ancestors of each member of children." |
| 5627 for (var i = 0; i < children.length; i++) { |
| 5628 fixDisallowedAncestors(children[i]); |
| 5629 } |
| 5630 |
| 5631 // "Abort these steps." |
| 5632 return; |
| 5633 } |
| 5634 |
| 5635 // "If current ancestor is not an editable indentation element, abort these |
| 5636 // steps." |
| 5637 if (!isEditable(currentAncestor) |
| 5638 || !isIndentationElement(currentAncestor)) { |
| 5639 return; |
| 5640 } |
| 5641 |
| 5642 // "Append current ancestor to ancestor list." |
| 5643 ancestorList.push(currentAncestor); |
| 5644 |
| 5645 // "Let original ancestor be current ancestor." |
| 5646 var originalAncestor = currentAncestor; |
| 5647 |
| 5648 // "While ancestor list is not empty:" |
| 5649 while (ancestorList.length) { |
| 5650 // "Let current ancestor be the last member of ancestor list." |
| 5651 // |
| 5652 // "Remove the last member of ancestor list." |
| 5653 currentAncestor = ancestorList.pop(); |
| 5654 |
| 5655 // "Let target be the child of current ancestor that is equal to either |
| 5656 // node or the last member of ancestor list." |
| 5657 var target = node.parentNode == currentAncestor |
| 5658 ? node |
| 5659 : ancestorList[ancestorList.length - 1]; |
| 5660 |
| 5661 // "If target is an inline node that is not a br, and its nextSibling |
| 5662 // is a br, remove target's nextSibling from its parent." |
| 5663 if (isInlineNode(target) |
| 5664 && !isHtmlElement(target, "BR") |
| 5665 && isHtmlElement(target.nextSibling, "BR")) { |
| 5666 target.parentNode.removeChild(target.nextSibling); |
| 5667 } |
| 5668 |
| 5669 // "Let preceding siblings be the preceding siblings of target, and let |
| 5670 // following siblings be the following siblings of target." |
| 5671 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, get
NodeIndex(target)); |
| 5672 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + ge
tNodeIndex(target)); |
| 5673 |
| 5674 // "Indent preceding siblings." |
| 5675 indentNodes(precedingSiblings); |
| 5676 |
| 5677 // "Indent following siblings." |
| 5678 indentNodes(followingSiblings); |
| 5679 } |
| 5680 |
| 5681 // "Outdent original ancestor." |
| 5682 outdentNode(originalAncestor); |
| 5683 } |
| 5684 |
| 5685 |
| 5686 //@} |
| 5687 ///// Toggling lists ///// |
| 5688 //@{ |
| 5689 |
| 5690 function toggleLists(tagName) { |
| 5691 // "Let mode be "disable" if the selection's list state is tag name, and |
| 5692 // "enable" otherwise." |
| 5693 var mode = getSelectionListState() == tagName ? "disable" : "enable"; |
| 5694 |
| 5695 var range = getActiveRange(); |
| 5696 tagName = tagName.toUpperCase(); |
| 5697 |
| 5698 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is |
| 5699 // "ol"." |
| 5700 var otherTagName = tagName == "OL" ? "UL" : "OL"; |
| 5701 |
| 5702 // "Let items be a list of all lis that are ancestor containers of the |
| 5703 // range's start and/or end node." |
| 5704 // |
| 5705 // It's annoying to get this in tree order using functional stuff without |
| 5706 // doing getDescendants(document), which is slow, so I do it imperatively. |
| 5707 var items = []; |
| 5708 (function(){ |
| 5709 for ( |
| 5710 var ancestorContainer = range.endContainer; |
| 5711 ancestorContainer != range.commonAncestorContainer; |
| 5712 ancestorContainer = ancestorContainer.parentNode |
| 5713 ) { |
| 5714 if (isHtmlElement(ancestorContainer, "li")) { |
| 5715 items.unshift(ancestorContainer); |
| 5716 } |
| 5717 } |
| 5718 for ( |
| 5719 var ancestorContainer = range.startContainer; |
| 5720 ancestorContainer; |
| 5721 ancestorContainer = ancestorContainer.parentNode |
| 5722 ) { |
| 5723 if (isHtmlElement(ancestorContainer, "li")) { |
| 5724 items.unshift(ancestorContainer); |
| 5725 } |
| 5726 } |
| 5727 })(); |
| 5728 |
| 5729 // "For each item in items, normalize sublists of item." |
| 5730 items.forEach(normalizeSublists); |
| 5731 |
| 5732 // "Block-extend the range, and let new range be the result." |
| 5733 var newRange = blockExtend(range); |
| 5734 |
| 5735 // "If mode is "enable", then let lists to convert consist of every |
| 5736 // editable HTML element with local name other tag name that is contained |
| 5737 // in new range, and for every list in lists to convert:" |
| 5738 if (mode == "enable") { |
| 5739 getAllContainedNodes(newRange, function(node) { |
| 5740 return isEditable(node) |
| 5741 && isHtmlElement(node, otherTagName); |
| 5742 }).forEach(function(list) { |
| 5743 // "If list's previousSibling or nextSibling is an editable HTML |
| 5744 // element with local name tag name:" |
| 5745 if ((isEditable(list.previousSibling) && isHtmlElement(list.previous
Sibling, tagName)) |
| 5746 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling,
tagName))) { |
| 5747 // "Let children be list's children." |
| 5748 var children = [].slice.call(list.childNodes); |
| 5749 |
| 5750 // "Record the values of children, and let values be the |
| 5751 // result." |
| 5752 var values = recordValues(children); |
| 5753 |
| 5754 // "Split the parent of children." |
| 5755 splitParent(children); |
| 5756 |
| 5757 // "Wrap children, with sibling criteria returning true for an |
| 5758 // HTML element with local name tag name and false otherwise." |
| 5759 wrap(children, function(node) { return isHtmlElement(node, tagNa
me) }); |
| 5760 |
| 5761 // "Restore the values from values." |
| 5762 restoreValues(values); |
| 5763 |
| 5764 // "Otherwise, set the tag name of list to tag name." |
| 5765 } else { |
| 5766 setTagName(list, tagName); |
| 5767 } |
| 5768 }); |
| 5769 } |
| 5770 |
| 5771 // "Let node list be a list of nodes, initially empty." |
| 5772 // |
| 5773 // "For each node node contained in new range, if node is editable; the |
| 5774 // last member of node list (if any) is not an ancestor of node; node |
| 5775 // is not an indentation element; and either node is an ol or ul, or its |
| 5776 // parent is an ol or ul, or it is an allowed child of "li"; then append |
| 5777 // node to node list." |
| 5778 var nodeList = getContainedNodes(newRange, function(node) { |
| 5779 return isEditable(node) |
| 5780 && !isIndentationElement(node) |
| 5781 && (isHtmlElement(node, ["OL", "UL"]) |
| 5782 || isHtmlElement(node.parentNode, ["OL", "UL"]) |
| 5783 || isAllowedChild(node, "li")); |
| 5784 }); |
| 5785 |
| 5786 // "If mode is "enable", remove from node list any ol or ul whose parent is |
| 5787 // not also an ol or ul." |
| 5788 if (mode == "enable") { |
| 5789 nodeList = nodeList.filter(function(node) { |
| 5790 return !isHtmlElement(node, ["ol", "ul"]) |
| 5791 || isHtmlElement(node.parentNode, ["ol", "ul"]); |
| 5792 }); |
| 5793 } |
| 5794 |
| 5795 // "If mode is "disable", then while node list is not empty:" |
| 5796 if (mode == "disable") { |
| 5797 while (nodeList.length) { |
| 5798 // "Let sublist be an empty list of nodes." |
| 5799 var sublist = []; |
| 5800 |
| 5801 // "Remove the first member from node list and append it to |
| 5802 // sublist." |
| 5803 sublist.push(nodeList.shift()); |
| 5804 |
| 5805 // "If the first member of sublist is an HTML element with local |
| 5806 // name tag name, outdent it and continue this loop from the |
| 5807 // beginning." |
| 5808 if (isHtmlElement(sublist[0], tagName)) { |
| 5809 outdentNode(sublist[0]); |
| 5810 continue; |
| 5811 } |
| 5812 |
| 5813 // "While node list is not empty, and the first member of node list |
| 5814 // is the nextSibling of the last member of sublist and is not an |
| 5815 // HTML element with local name tag name, remove the first member |
| 5816 // from node list and append it to sublist." |
| 5817 while (nodeList.length |
| 5818 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
| 5819 && !isHtmlElement(nodeList[0], tagName)) { |
| 5820 sublist.push(nodeList.shift()); |
| 5821 } |
| 5822 |
| 5823 // "Record the values of sublist, and let values be the result." |
| 5824 var values = recordValues(sublist); |
| 5825 |
| 5826 // "Split the parent of sublist." |
| 5827 splitParent(sublist); |
| 5828 |
| 5829 // "Fix disallowed ancestors of each member of sublist." |
| 5830 for (var i = 0; i < sublist.length; i++) { |
| 5831 fixDisallowedAncestors(sublist[i]); |
| 5832 } |
| 5833 |
| 5834 // "Restore the values from values." |
| 5835 restoreValues(values); |
| 5836 } |
| 5837 |
| 5838 // "Otherwise, while node list is not empty:" |
| 5839 } else { |
| 5840 while (nodeList.length) { |
| 5841 // "Let sublist be an empty list of nodes." |
| 5842 var sublist = []; |
| 5843 |
| 5844 // "While either sublist is empty, or node list is not empty and |
| 5845 // its first member is the nextSibling of sublist's last member:" |
| 5846 while (!sublist.length |
| 5847 || (nodeList.length |
| 5848 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { |
| 5849 // "If node list's first member is a p or div, set the tag name |
| 5850 // of node list's first member to "li", and append the result |
| 5851 // to sublist. Remove the first member from node list." |
| 5852 if (isHtmlElement(nodeList[0], ["p", "div"])) { |
| 5853 sublist.push(setTagName(nodeList[0], "li")); |
| 5854 nodeList.shift(); |
| 5855 |
| 5856 // "Otherwise, if the first member of node list is an li or ol |
| 5857 // or ul, remove it from node list and append it to sublist." |
| 5858 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) { |
| 5859 sublist.push(nodeList.shift()); |
| 5860 |
| 5861 // "Otherwise:" |
| 5862 } else { |
| 5863 // "Let nodes to wrap be a list of nodes, initially empty." |
| 5864 var nodesToWrap = []; |
| 5865 |
| 5866 // "While nodes to wrap is empty, or node list is not empty |
| 5867 // and its first member is the nextSibling of nodes to |
| 5868 // wrap's last member and the first member of node list is |
| 5869 // an inline node and the last member of nodes to wrap is |
| 5870 // an inline node other than a br, remove the first member |
| 5871 // from node list and append it to nodes to wrap." |
| 5872 while (!nodesToWrap.length |
| 5873 || (nodeList.length |
| 5874 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSi
bling |
| 5875 && isInlineNode(nodeList[0]) |
| 5876 && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) |
| 5877 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br")
)) { |
| 5878 nodesToWrap.push(nodeList.shift()); |
| 5879 } |
| 5880 |
| 5881 // "Wrap nodes to wrap, with new parent instructions |
| 5882 // returning the result of calling createElement("li") on |
| 5883 // the context object. Append the result to sublist." |
| 5884 sublist.push(wrap(nodesToWrap, |
| 5885 undefined, |
| 5886 function() { return document.createElement("li") })); |
| 5887 } |
| 5888 } |
| 5889 |
| 5890 // "If sublist's first member's parent is an HTML element with |
| 5891 // local name tag name, or if every member of sublist is an ol or |
| 5892 // ul, continue this loop from the beginning." |
| 5893 if (isHtmlElement(sublist[0].parentNode, tagName) |
| 5894 || sublist.every(function(node) { return isHtmlElement(node, ["ol",
"ul"]) })) { |
| 5895 continue; |
| 5896 } |
| 5897 |
| 5898 // "If sublist's first member's parent is an HTML element with |
| 5899 // local name other tag name:" |
| 5900 if (isHtmlElement(sublist[0].parentNode, otherTagName)) { |
| 5901 // "Record the values of sublist, and let values be the |
| 5902 // result." |
| 5903 var values = recordValues(sublist); |
| 5904 |
| 5905 // "Split the parent of sublist." |
| 5906 splitParent(sublist); |
| 5907 |
| 5908 // "Wrap sublist, with sibling criteria returning true for an |
| 5909 // HTML element with local name tag name and false otherwise, |
| 5910 // and new parent instructions returning the result of calling |
| 5911 // createElement(tag name) on the context object." |
| 5912 wrap(sublist, |
| 5913 function(node) { return isHtmlElement(node, tagName) }, |
| 5914 function() { return document.createElement(tagName) }); |
| 5915 |
| 5916 // "Restore the values from values." |
| 5917 restoreValues(values); |
| 5918 |
| 5919 // "Continue this loop from the beginning." |
| 5920 continue; |
| 5921 } |
| 5922 |
| 5923 // "Wrap sublist, with sibling criteria returning true for an HTML |
| 5924 // element with local name tag name and false otherwise, and new |
| 5925 // parent instructions being the following:" |
| 5926 // . . . |
| 5927 // "Fix disallowed ancestors of the previous step's result." |
| 5928 fixDisallowedAncestors(wrap(sublist, |
| 5929 function(node) { return isHtmlElement(node, tagName) }, |
| 5930 function() { |
| 5931 // "If sublist's first member's parent is not an editable |
| 5932 // simple indentation element, or sublist's first member's |
| 5933 // parent's previousSibling is not an editable HTML element |
| 5934 // with local name tag name, call createElement(tag name) |
| 5935 // on the context object and return the result." |
| 5936 if (!isEditable(sublist[0].parentNode) |
| 5937 || !isSimpleIndentationElement(sublist[0].parentNode) |
| 5938 || !isEditable(sublist[0].parentNode.previousSibling) |
| 5939 || !isHtmlElement(sublist[0].parentNode.previousSibling, tag
Name)) { |
| 5940 return document.createElement(tagName); |
| 5941 } |
| 5942 |
| 5943 // "Let list be sublist's first member's parent's |
| 5944 // previousSibling." |
| 5945 var list = sublist[0].parentNode.previousSibling; |
| 5946 |
| 5947 // "Normalize sublists of list's lastChild." |
| 5948 normalizeSublists(list.lastChild); |
| 5949 |
| 5950 // "If list's lastChild is not an editable HTML element |
| 5951 // with local name tag name, call createElement(tag name) |
| 5952 // on the context object, and append the result as the last |
| 5953 // child of list." |
| 5954 if (!isEditable(list.lastChild) |
| 5955 || !isHtmlElement(list.lastChild, tagName)) { |
| 5956 list.appendChild(document.createElement(tagName)); |
| 5957 } |
| 5958 |
| 5959 // "Return the last child of list." |
| 5960 return list.lastChild; |
| 5961 } |
| 5962 )); |
| 5963 } |
| 5964 } |
| 5965 } |
| 5966 |
| 5967 |
| 5968 //@} |
| 5969 ///// Justifying the selection ///// |
| 5970 //@{ |
| 5971 |
| 5972 function justifySelection(alignment) { |
| 5973 // "Block-extend the active range, and let new range be the result." |
| 5974 var newRange = blockExtend(globalRange); |
| 5975 |
| 5976 // "Let element list be a list of all editable Elements contained in new |
| 5977 // range that either has an attribute in the HTML namespace whose local |
| 5978 // name is "align", or has a style attribute that sets "text-align", or is |
| 5979 // a center." |
| 5980 var elementList = getAllContainedNodes(newRange, function(node) { |
| 5981 return node.nodeType == Node.ELEMENT_NODE |
| 5982 && isEditable(node) |
| 5983 // Ignoring namespaces here |
| 5984 && ( |
| 5985 node.hasAttribute("align") |
| 5986 || node.style.textAlign != "" |
| 5987 || isHtmlElement(node, "center") |
| 5988 ); |
| 5989 }); |
| 5990 |
| 5991 // "For each element in element list:" |
| 5992 for (var i = 0; i < elementList.length; i++) { |
| 5993 var element = elementList[i]; |
| 5994 |
| 5995 // "If element has an attribute in the HTML namespace whose local name |
| 5996 // is "align", remove that attribute." |
| 5997 element.removeAttribute("align"); |
| 5998 |
| 5999 // "Unset the CSS property "text-align" on element, if it's set by a |
| 6000 // style attribute." |
| 6001 element.style.textAlign = ""; |
| 6002 if (element.getAttribute("style") == "") { |
| 6003 element.removeAttribute("style"); |
| 6004 } |
| 6005 |
| 6006 // "If element is a div or span or center with no attributes, remove |
| 6007 // it, preserving its descendants." |
| 6008 if (isHtmlElement(element, ["div", "span", "center"]) |
| 6009 && !element.attributes.length) { |
| 6010 removePreservingDescendants(element); |
| 6011 } |
| 6012 |
| 6013 // "If element is a center with one or more attributes, set the tag |
| 6014 // name of element to "div"." |
| 6015 if (isHtmlElement(element, "center") |
| 6016 && element.attributes.length) { |
| 6017 setTagName(element, "div"); |
| 6018 } |
| 6019 } |
| 6020 |
| 6021 // "Block-extend the active range, and let new range be the result." |
| 6022 newRange = blockExtend(globalRange); |
| 6023 |
| 6024 // "Let node list be a list of nodes, initially empty." |
| 6025 var nodeList = []; |
| 6026 |
| 6027 // "For each node node contained in new range, append node to node list if |
| 6028 // the last member of node list (if any) is not an ancestor of node; node |
| 6029 // is editable; node is an allowed child of "div"; and node's alignment |
| 6030 // value is not alignment." |
| 6031 nodeList = getContainedNodes(newRange, function(node) { |
| 6032 return isEditable(node) |
| 6033 && isAllowedChild(node, "div") |
| 6034 && getAlignmentValue(node) != alignment; |
| 6035 }); |
| 6036 |
| 6037 // "While node list is not empty:" |
| 6038 while (nodeList.length) { |
| 6039 // "Let sublist be a list of nodes, initially empty." |
| 6040 var sublist = []; |
| 6041 |
| 6042 // "Remove the first member of node list and append it to sublist." |
| 6043 sublist.push(nodeList.shift()); |
| 6044 |
| 6045 // "While node list is not empty, and the first member of node list is |
| 6046 // the nextSibling of the last member of sublist, remove the first |
| 6047 // member of node list and append it to sublist." |
| 6048 while (nodeList.length |
| 6049 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
| 6050 sublist.push(nodeList.shift()); |
| 6051 } |
| 6052 |
| 6053 // "Wrap sublist. Sibling criteria returns true for any div that has |
| 6054 // one or both of the following two attributes and no other attributes, |
| 6055 // and false otherwise:" |
| 6056 // |
| 6057 // * "An align attribute whose value is an ASCII case-insensitive |
| 6058 // match for alignment. |
| 6059 // * "A style attribute which sets exactly one CSS property |
| 6060 // (including unrecognized or invalid attributes), which is |
| 6061 // "text-align", which is set to alignment. |
| 6062 // |
| 6063 // "New parent instructions are to call createElement("div") on the |
| 6064 // context object, then set its CSS property "text-align" to alignment |
| 6065 // and return the result." |
| 6066 wrap(sublist, |
| 6067 function(node) { |
| 6068 return isHtmlElement(node, "div") |
| 6069 && [].every.call(node.attributes, function(attr) { |
| 6070 return (attr.name == "align" && attr.value.toLowerCase()
== alignment) |
| 6071 || (attr.name == "style" && node.style.length == 1 &
& node.style.textAlign == alignment); |
| 6072 }); |
| 6073 }, |
| 6074 function() { |
| 6075 var newParent = document.createElement("div"); |
| 6076 newParent.setAttribute("style", "text-align: " + alignment); |
| 6077 return newParent; |
| 6078 } |
| 6079 ); |
| 6080 } |
| 6081 } |
| 6082 |
| 6083 |
| 6084 //@} |
| 6085 ///// Automatic linking ///// |
| 6086 //@{ |
| 6087 // "An autolinkable URL is a string of the following form:" |
| 6088 var autolinkableUrlRegexp = |
| 6089 // "Either a string matching the scheme pattern from RFC 3986 section 3.1 |
| 6090 // followed by the literal string ://, or the literal string mailto:; |
| 6091 // followed by" |
| 6092 // |
| 6093 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
| 6094 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)" |
| 6095 // "Zero or more characters other than space characters; followed by" |
| 6096 + "[^ \t\n\f\r]*" |
| 6097 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}." |
| 6098 + "[^!\"'(),\\-.:;<>[\\]`{}]"; |
| 6099 |
| 6100 // "A valid e-mail address is a string that matches the ABNF production 1*( |
| 6101 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC |
| 6102 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5." |
| 6103 // |
| 6104 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / |
| 6105 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" |
| 6106 // |
| 6107 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> |
| 6108 //<let-dig-hyp> ::= <let-dig> | "-" |
| 6109 //<let-dig> ::= <letter> | <digit> |
| 6110 var validEmailRegexp = |
| 6111 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*"; |
| 6112 |
| 6113 function autolink(node, endOffset) { |
| 6114 // "While (node, end offset)'s previous equivalent point is not null, set |
| 6115 // it to its previous equivalent point." |
| 6116 while (getPreviousEquivalentPoint(node, endOffset)) { |
| 6117 var prev = getPreviousEquivalentPoint(node, endOffset); |
| 6118 node = prev[0]; |
| 6119 endOffset = prev[1]; |
| 6120 } |
| 6121 |
| 6122 // "If node is not a Text node, or has an a ancestor, do nothing and abort |
| 6123 // these steps." |
| 6124 if (node.nodeType != Node.TEXT_NODE |
| 6125 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancesto
r, "a") })) { |
| 6126 return; |
| 6127 } |
| 6128 |
| 6129 // "Let search be the largest substring of node's data whose end is end |
| 6130 // offset and that contains no space characters." |
| 6131 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0]; |
| 6132 |
| 6133 // "If some substring of search is an autolinkable URL:" |
| 6134 if (new RegExp(autolinkableUrlRegexp).test(search)) { |
| 6135 // "While there is no substring of node's data ending at end offset |
| 6136 // that is an autolinkable URL, decrement end offset." |
| 6137 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData
(0, endOffset)))) { |
| 6138 endOffset--; |
| 6139 } |
| 6140 |
| 6141 // "Let start offset be the start index of the longest substring of |
| 6142 // node's data that is an autolinkable URL ending at end offset." |
| 6143 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.subs
tringData(0, endOffset)).index; |
| 6144 |
| 6145 // "Let href be the substring of node's data starting at start offset |
| 6146 // and ending at end offset." |
| 6147 var href = node.substringData(startOffset, endOffset - startOffset); |
| 6148 |
| 6149 // "Otherwise, if some substring of search is a valid e-mail address:" |
| 6150 } else if (new RegExp(validEmailRegexp).test(search)) { |
| 6151 // "While there is no substring of node's data ending at end offset |
| 6152 // that is a valid e-mail address, decrement end offset." |
| 6153 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, e
ndOffset)))) { |
| 6154 endOffset--; |
| 6155 } |
| 6156 |
| 6157 // "Let start offset be the start index of the longest substring of |
| 6158 // node's data that is a valid e-mail address ending at end offset." |
| 6159 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substring
Data(0, endOffset)).index; |
| 6160 |
| 6161 // "Let href be "mailto:" concatenated with the substring of node's |
| 6162 // data starting at start offset and ending at end offset." |
| 6163 var href = "mailto:" + node.substringData(startOffset, endOffset - start
Offset); |
| 6164 |
| 6165 // "Otherwise, do nothing and abort these steps." |
| 6166 } else { |
| 6167 return; |
| 6168 } |
| 6169 |
| 6170 // "Let original range be the active range." |
| 6171 var originalRange = getActiveRange(); |
| 6172 |
| 6173 // "Create a new range with start (node, start offset) and end (node, end |
| 6174 // offset), and set the context object's selection's range to it." |
| 6175 var newRange = document.createRange(); |
| 6176 newRange.setStart(node, startOffset); |
| 6177 newRange.setEnd(node, endOffset); |
| 6178 getSelection().removeAllRanges(); |
| 6179 getSelection().addRange(newRange); |
| 6180 globalRange = newRange; |
| 6181 |
| 6182 // "Take the action for "createLink", with value equal to href." |
| 6183 commands.createlink.action(href); |
| 6184 |
| 6185 // "Set the context object's selection's range to original range." |
| 6186 getSelection().removeAllRanges(); |
| 6187 getSelection().addRange(originalRange); |
| 6188 globalRange = originalRange; |
| 6189 } |
| 6190 //@} |
| 6191 ///// The delete command ///// |
| 6192 //@{ |
| 6193 commands["delete"] = { |
| 6194 preservesOverrides: true, |
| 6195 action: function() { |
| 6196 // "If the active range is not collapsed, delete the selection and |
| 6197 // return true." |
| 6198 if (!getActiveRange().collapsed) { |
| 6199 deleteSelection(); |
| 6200 return true; |
| 6201 } |
| 6202 |
| 6203 // "Canonicalize whitespace at the active range's start." |
| 6204 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange()
.startOffset); |
| 6205 |
| 6206 // "Let node and offset be the active range's start node and offset." |
| 6207 var node = getActiveRange().startContainer; |
| 6208 var offset = getActiveRange().startOffset; |
| 6209 |
| 6210 // "Repeat the following steps:" |
| 6211 while (true) { |
| 6212 // "If offset is zero and node's previousSibling is an editable |
| 6213 // invisible node, remove node's previousSibling from its parent." |
| 6214 if (offset == 0 |
| 6215 && isEditable(node.previousSibling) |
| 6216 && isInvisible(node.previousSibling)) { |
| 6217 node.parentNode.removeChild(node.previousSibling); |
| 6218 |
| 6219 // "Otherwise, if node has a child with index offset − 1 and that |
| 6220 // child is an editable invisible node, remove that child from |
| 6221 // node, then subtract one from offset." |
| 6222 } else if (0 <= offset - 1 |
| 6223 && offset - 1 < node.childNodes.length |
| 6224 && isEditable(node.childNodes[offset - 1]) |
| 6225 && isInvisible(node.childNodes[offset - 1])) { |
| 6226 node.removeChild(node.childNodes[offset - 1]); |
| 6227 offset--; |
| 6228 |
| 6229 // "Otherwise, if offset is zero and node is an inline node, or if |
| 6230 // node is an invisible node, set offset to the index of node, then |
| 6231 // set node to its parent." |
| 6232 } else if ((offset == 0 |
| 6233 && isInlineNode(node)) |
| 6234 || isInvisible(node)) { |
| 6235 offset = getNodeIndex(node); |
| 6236 node = node.parentNode; |
| 6237 |
| 6238 // "Otherwise, if node has a child with index offset − 1 and that |
| 6239 // child is an editable a, remove that child from node, preserving |
| 6240 // its descendants. Then return true." |
| 6241 } else if (0 <= offset - 1 |
| 6242 && offset - 1 < node.childNodes.length |
| 6243 && isEditable(node.childNodes[offset - 1]) |
| 6244 && isHtmlElement(node.childNodes[offset - 1], "a")) { |
| 6245 removePreservingDescendants(node.childNodes[offset - 1]); |
| 6246 return true; |
| 6247 |
| 6248 // "Otherwise, if node has a child with index offset − 1 and that |
| 6249 // child is not a block node or a br or an img, set node to that |
| 6250 // child, then set offset to the length of node." |
| 6251 } else if (0 <= offset - 1 |
| 6252 && offset - 1 < node.childNodes.length |
| 6253 && !isBlockNode(node.childNodes[offset - 1]) |
| 6254 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) { |
| 6255 node = node.childNodes[offset - 1]; |
| 6256 offset = getNodeLength(node); |
| 6257 |
| 6258 // "Otherwise, break from this loop." |
| 6259 } else { |
| 6260 break; |
| 6261 } |
| 6262 } |
| 6263 |
| 6264 // "If node is a Text node and offset is not zero, or if node is a |
| 6265 // block node that has a child with index offset − 1 and that child is |
| 6266 // a br or hr or img:" |
| 6267 if ((node.nodeType == Node.TEXT_NODE |
| 6268 && offset != 0) |
| 6269 || (isBlockNode(node) |
| 6270 && 0 <= offset - 1 |
| 6271 && offset - 1 < node.childNodes.length |
| 6272 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) { |
| 6273 // "Call collapse(node, offset) on the context object's Selection." |
| 6274 getSelection().collapse(node, offset); |
| 6275 getActiveRange().setEnd(node, offset); |
| 6276 |
| 6277 // "Call extend(node, offset − 1) on the context object's |
| 6278 // Selection." |
| 6279 getSelection().extend(node, offset - 1); |
| 6280 getActiveRange().setStart(node, offset - 1); |
| 6281 |
| 6282 // "Delete the selection." |
| 6283 deleteSelection(); |
| 6284 |
| 6285 // "Return true." |
| 6286 return true; |
| 6287 } |
| 6288 |
| 6289 // "If node is an inline node, return true." |
| 6290 if (isInlineNode(node)) { |
| 6291 return true; |
| 6292 } |
| 6293 |
| 6294 // "If node is an li or dt or dd and is the first child of its parent, |
| 6295 // and offset is zero:" |
| 6296 if (isHtmlElement(node, ["li", "dt", "dd"]) |
| 6297 && node == node.parentNode.firstChild |
| 6298 && offset == 0) { |
| 6299 // "Let items be a list of all lis that are ancestors of node." |
| 6300 // |
| 6301 // Remember, must be in tree order. |
| 6302 var items = []; |
| 6303 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.p
arentNode) { |
| 6304 if (isHtmlElement(ancestor, "li")) { |
| 6305 items.unshift(ancestor); |
| 6306 } |
| 6307 } |
| 6308 |
| 6309 // "Normalize sublists of each item in items." |
| 6310 for (var i = 0; i < items.length; i++) { |
| 6311 normalizeSublists(items[i]); |
| 6312 } |
| 6313 |
| 6314 // "Record the values of the one-node list consisting of node, and |
| 6315 // let values be the result." |
| 6316 var values = recordValues([node]); |
| 6317 |
| 6318 // "Split the parent of the one-node list consisting of node." |
| 6319 splitParent([node]); |
| 6320 |
| 6321 // "Restore the values from values." |
| 6322 restoreValues(values); |
| 6323 |
| 6324 // "If node is a dd or dt, and it is not an allowed child of any of |
| 6325 // its ancestors in the same editing host, set the tag name of node |
| 6326 // to the default single-line container name and let node be the |
| 6327 // result." |
| 6328 if (isHtmlElement(node, ["dd", "dt"]) |
| 6329 && getAncestors(node).every(function(ancestor) { |
| 6330 return !inSameEditingHost(node, ancestor) |
| 6331 || !isAllowedChild(node, ancestor) |
| 6332 })) { |
| 6333 node = setTagName(node, defaultSingleLineContainerName); |
| 6334 } |
| 6335 |
| 6336 // "Fix disallowed ancestors of node." |
| 6337 fixDisallowedAncestors(node); |
| 6338 |
| 6339 // "Return true." |
| 6340 return true; |
| 6341 } |
| 6342 |
| 6343 // "Let start node equal node and let start offset equal offset." |
| 6344 var startNode = node; |
| 6345 var startOffset = offset; |
| 6346 |
| 6347 // "Repeat the following steps:" |
| 6348 while (true) { |
| 6349 // "If start offset is zero, set start offset to the index of start |
| 6350 // node and then set start node to its parent." |
| 6351 if (startOffset == 0) { |
| 6352 startOffset = getNodeIndex(startNode); |
| 6353 startNode = startNode.parentNode; |
| 6354 |
| 6355 // "Otherwise, if start node has an editable invisible child with |
| 6356 // index start offset minus one, remove it from start node and |
| 6357 // subtract one from start offset." |
| 6358 } else if (0 <= startOffset - 1 |
| 6359 && startOffset - 1 < startNode.childNodes.length |
| 6360 && isEditable(startNode.childNodes[startOffset - 1]) |
| 6361 && isInvisible(startNode.childNodes[startOffset - 1])) { |
| 6362 startNode.removeChild(startNode.childNodes[startOffset - 1]); |
| 6363 startOffset--; |
| 6364 |
| 6365 // "Otherwise, break from this loop." |
| 6366 } else { |
| 6367 break; |
| 6368 } |
| 6369 } |
| 6370 |
| 6371 // "If offset is zero, and node has an editable ancestor container in |
| 6372 // the same editing host that's an indentation element:" |
| 6373 if (offset == 0 |
| 6374 && getAncestors(node).concat(node).filter(function(ancestor) { |
| 6375 return isEditable(ancestor) |
| 6376 && inSameEditingHost(ancestor, node) |
| 6377 && isIndentationElement(ancestor); |
| 6378 }).length) { |
| 6379 // "Block-extend the range whose start and end are both (node, 0), |
| 6380 // and let new range be the result." |
| 6381 var newRange = document.createRange(); |
| 6382 newRange.setStart(node, 0); |
| 6383 newRange = blockExtend(newRange); |
| 6384 |
| 6385 // "Let node list be a list of nodes, initially empty." |
| 6386 // |
| 6387 // "For each node current node contained in new range, append |
| 6388 // current node to node list if the last member of node list (if |
| 6389 // any) is not an ancestor of current node, and current node is |
| 6390 // editable but has no editable descendants." |
| 6391 var nodeList = getContainedNodes(newRange, function(currentNode) { |
| 6392 return isEditable(currentNode) |
| 6393 && !hasEditableDescendants(currentNode); |
| 6394 }); |
| 6395 |
| 6396 // "Outdent each node in node list." |
| 6397 for (var i = 0; i < nodeList.length; i++) { |
| 6398 outdentNode(nodeList[i]); |
| 6399 } |
| 6400 |
| 6401 // "Return true." |
| 6402 return true; |
| 6403 } |
| 6404 |
| 6405 // "If the child of start node with index start offset is a table, |
| 6406 // return true." |
| 6407 if (isHtmlElement(startNode.childNodes[startOffset], "table")) { |
| 6408 return true; |
| 6409 } |
| 6410 |
| 6411 // "If start node has a child with index start offset − 1, and that |
| 6412 // child is a table:" |
| 6413 if (0 <= startOffset - 1 |
| 6414 && startOffset - 1 < startNode.childNodes.length |
| 6415 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) { |
| 6416 // "Call collapse(start node, start offset − 1) on the context |
| 6417 // object's Selection." |
| 6418 getSelection().collapse(startNode, startOffset - 1); |
| 6419 getActiveRange().setStart(startNode, startOffset - 1); |
| 6420 |
| 6421 // "Call extend(start node, start offset) on the context object's |
| 6422 // Selection." |
| 6423 getSelection().extend(startNode, startOffset); |
| 6424 getActiveRange().setEnd(startNode, startOffset); |
| 6425 |
| 6426 // "Return true." |
| 6427 return true; |
| 6428 } |
| 6429 |
| 6430 // "If offset is zero; and either the child of start node with index |
| 6431 // start offset minus one is an hr, or the child is a br whose |
| 6432 // previousSibling is either a br or not an inline node:" |
| 6433 if (offset == 0 |
| 6434 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr") |
| 6435 || ( |
| 6436 isHtmlElement(startNode.childNodes[startOffset - 1], "br") |
| 6437 && ( |
| 6438 isHtmlElement(startNode.childNodes[startOffset - 1].previous
Sibling, "br") |
| 6439 || !isInlineNode(startNode.childNodes[startOffset - 1].previ
ousSibling) |
| 6440 ) |
| 6441 ) |
| 6442 )) { |
| 6443 // "Call collapse(start node, start offset − 1) on the context |
| 6444 // object's Selection." |
| 6445 getSelection().collapse(startNode, startOffset - 1); |
| 6446 getActiveRange().setStart(startNode, startOffset - 1); |
| 6447 |
| 6448 // "Call extend(start node, start offset) on the context object's |
| 6449 // Selection." |
| 6450 getSelection().extend(startNode, startOffset); |
| 6451 getActiveRange().setEnd(startNode, startOffset); |
| 6452 |
| 6453 // "Delete the selection." |
| 6454 deleteSelection(); |
| 6455 |
| 6456 // "Call collapse(node, offset) on the Selection." |
| 6457 getSelection().collapse(node, offset); |
| 6458 getActiveRange().setStart(node, offset); |
| 6459 getActiveRange().collapse(true); |
| 6460 |
| 6461 // "Return true." |
| 6462 return true; |
| 6463 } |
| 6464 |
| 6465 // "If the child of start node with index start offset is an li or dt |
| 6466 // or dd, and that child's firstChild is an inline node, and start |
| 6467 // offset is not zero:" |
| 6468 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
| 6469 && isInlineNode(startNode.childNodes[startOffset].firstChild) |
| 6470 && startOffset != 0) { |
| 6471 // "Let previous item be the child of start node with index start |
| 6472 // offset minus one." |
| 6473 var previousItem = startNode.childNodes[startOffset - 1]; |
| 6474 |
| 6475 // "If previous item's lastChild is an inline node other than a br, |
| 6476 // call createElement("br") on the context object and append the |
| 6477 // result as the last child of previous item." |
| 6478 if (isInlineNode(previousItem.lastChild) |
| 6479 && !isHtmlElement(previousItem.lastChild, "br")) { |
| 6480 previousItem.appendChild(document.createElement("br")); |
| 6481 } |
| 6482 |
| 6483 // "If previous item's lastChild is an inline node, call |
| 6484 // createElement("br") on the context object and append the result |
| 6485 // as the last child of previous item." |
| 6486 if (isInlineNode(previousItem.lastChild)) { |
| 6487 previousItem.appendChild(document.createElement("br")); |
| 6488 } |
| 6489 } |
| 6490 |
| 6491 // "If start node's child with index start offset is an li or dt or dd, |
| 6492 // and that child's previousSibling is also an li or dt or dd:" |
| 6493 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
| 6494 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li
", "dt", "dd"])) { |
| 6495 // "Call cloneRange() on the active range, and let original range |
| 6496 // be the result." |
| 6497 // |
| 6498 // We need to add it to extraRanges so it will actually get updated |
| 6499 // when moving preserving ranges. |
| 6500 var originalRange = getActiveRange().cloneRange(); |
| 6501 extraRanges.push(originalRange); |
| 6502 |
| 6503 // "Set start node to its child with index start offset − 1." |
| 6504 startNode = startNode.childNodes[startOffset - 1]; |
| 6505 |
| 6506 // "Set start offset to start node's length." |
| 6507 startOffset = getNodeLength(startNode); |
| 6508 |
| 6509 // "Set node to start node's nextSibling." |
| 6510 node = startNode.nextSibling; |
| 6511 |
| 6512 // "Call collapse(start node, start offset) on the context object's |
| 6513 // Selection." |
| 6514 getSelection().collapse(startNode, startOffset); |
| 6515 getActiveRange().setStart(startNode, startOffset); |
| 6516 |
| 6517 // "Call extend(node, 0) on the context object's Selection." |
| 6518 getSelection().extend(node, 0); |
| 6519 getActiveRange().setEnd(node, 0); |
| 6520 |
| 6521 // "Delete the selection." |
| 6522 deleteSelection(); |
| 6523 |
| 6524 // "Call removeAllRanges() on the context object's Selection." |
| 6525 getSelection().removeAllRanges(); |
| 6526 |
| 6527 // "Call addRange(original range) on the context object's |
| 6528 // Selection." |
| 6529 getSelection().addRange(originalRange); |
| 6530 getActiveRange().setStart(originalRange.startContainer, originalRang
e.startOffset); |
| 6531 getActiveRange().setEnd(originalRange.endContainer, originalRange.en
dOffset); |
| 6532 |
| 6533 // "Return true." |
| 6534 extraRanges.pop(); |
| 6535 return true; |
| 6536 } |
| 6537 |
| 6538 // "While start node has a child with index start offset minus one:" |
| 6539 while (0 <= startOffset - 1 |
| 6540 && startOffset - 1 < startNode.childNodes.length) { |
| 6541 // "If start node's child with index start offset minus one is |
| 6542 // editable and invisible, remove it from start node, then subtract |
| 6543 // one from start offset." |
| 6544 if (isEditable(startNode.childNodes[startOffset - 1]) |
| 6545 && isInvisible(startNode.childNodes[startOffset - 1])) { |
| 6546 startNode.removeChild(startNode.childNodes[startOffset - 1]); |
| 6547 startOffset--; |
| 6548 |
| 6549 // "Otherwise, set start node to its child with index start offset |
| 6550 // minus one, then set start offset to the length of start node." |
| 6551 } else { |
| 6552 startNode = startNode.childNodes[startOffset - 1]; |
| 6553 startOffset = getNodeLength(startNode); |
| 6554 } |
| 6555 } |
| 6556 |
| 6557 // "Call collapse(start node, start offset) on the context object's |
| 6558 // Selection." |
| 6559 getSelection().collapse(startNode, startOffset); |
| 6560 getActiveRange().setStart(startNode, startOffset); |
| 6561 |
| 6562 // "Call extend(node, offset) on the context object's Selection." |
| 6563 getSelection().extend(node, offset); |
| 6564 getActiveRange().setEnd(node, offset); |
| 6565 |
| 6566 // "Delete the selection, with direction "backward"." |
| 6567 deleteSelection({direction: "backward"}); |
| 6568 |
| 6569 // "Return true." |
| 6570 return true; |
| 6571 } |
| 6572 }; |
| 6573 |
| 6574 //@} |
| 6575 ///// The formatBlock command ///// |
| 6576 //@{ |
| 6577 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", |
| 6578 // "h4", "h5", "h6", "p", or "pre"." |
| 6579 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", |
| 6580 "h4", "h5", "h6", "p", "pre"]; |
| 6581 |
| 6582 commands.formatblock = { |
| 6583 preservesOverrides: true, |
| 6584 action: function(value) { |
| 6585 // "If value begins with a "<" character and ends with a ">" character, |
| 6586 // remove the first and last characters from it." |
| 6587 if (/^<.*>$/.test(value)) { |
| 6588 value = value.slice(1, -1); |
| 6589 } |
| 6590 |
| 6591 // "Let value be converted to ASCII lowercase." |
| 6592 value = value.toLowerCase(); |
| 6593 |
| 6594 // "If value is not a formattable block name, return false." |
| 6595 if (formattableBlockNames.indexOf(value) == -1) { |
| 6596 return false; |
| 6597 } |
| 6598 |
| 6599 // "Block-extend the active range, and let new range be the result." |
| 6600 var newRange = blockExtend(getActiveRange()); |
| 6601 |
| 6602 // "Let node list be an empty list of nodes." |
| 6603 // |
| 6604 // "For each node node contained in new range, append node to node list |
| 6605 // if it is editable, the last member of original node list (if any) is |
| 6606 // not an ancestor of node, node is either a non-list single-line |
| 6607 // container or an allowed child of "p" or a dd or dt, and node is not |
| 6608 // the ancestor of a prohibited paragraph child." |
| 6609 var nodeList = getContainedNodes(newRange, function(node) { |
| 6610 return isEditable(node) |
| 6611 && (isNonListSingleLineContainer(node) |
| 6612 || isAllowedChild(node, "p") |
| 6613 || isHtmlElement(node, ["dd", "dt"])) |
| 6614 && !getDescendants(node).some(isProhibitedParagraphChild); |
| 6615 }); |
| 6616 |
| 6617 // "Record the values of node list, and let values be the result." |
| 6618 var values = recordValues(nodeList); |
| 6619 |
| 6620 // "For each node in node list, while node is the descendant of an |
| 6621 // editable HTML element in the same editing host, whose local name is |
| 6622 // a formattable block name, and which is not the ancestor of a |
| 6623 // prohibited paragraph child, split the parent of the one-node list |
| 6624 // consisting of node." |
| 6625 for (var i = 0; i < nodeList.length; i++) { |
| 6626 var node = nodeList[i]; |
| 6627 while (getAncestors(node).some(function(ancestor) { |
| 6628 return isEditable(ancestor) |
| 6629 && inSameEditingHost(ancestor, node) |
| 6630 && isHtmlElement(ancestor, formattableBlockNames) |
| 6631 && !getDescendants(ancestor).some(isProhibitedParagraphChild
); |
| 6632 })) { |
| 6633 splitParent([node]); |
| 6634 } |
| 6635 } |
| 6636 |
| 6637 // "Restore the values from values." |
| 6638 restoreValues(values); |
| 6639 |
| 6640 // "While node list is not empty:" |
| 6641 while (nodeList.length) { |
| 6642 var sublist; |
| 6643 |
| 6644 // "If the first member of node list is a single-line |
| 6645 // container:" |
| 6646 if (isSingleLineContainer(nodeList[0])) { |
| 6647 // "Let sublist be the children of the first member of node |
| 6648 // list." |
| 6649 sublist = [].slice.call(nodeList[0].childNodes); |
| 6650 |
| 6651 // "Record the values of sublist, and let values be the |
| 6652 // result." |
| 6653 var values = recordValues(sublist); |
| 6654 |
| 6655 // "Remove the first member of node list from its parent, |
| 6656 // preserving its descendants." |
| 6657 removePreservingDescendants(nodeList[0]); |
| 6658 |
| 6659 // "Restore the values from values." |
| 6660 restoreValues(values); |
| 6661 |
| 6662 // "Remove the first member from node list." |
| 6663 nodeList.shift(); |
| 6664 |
| 6665 // "Otherwise:" |
| 6666 } else { |
| 6667 // "Let sublist be an empty list of nodes." |
| 6668 sublist = []; |
| 6669 |
| 6670 // "Remove the first member of node list and append it to |
| 6671 // sublist." |
| 6672 sublist.push(nodeList.shift()); |
| 6673 |
| 6674 // "While node list is not empty, and the first member of |
| 6675 // node list is the nextSibling of the last member of |
| 6676 // sublist, and the first member of node list is not a |
| 6677 // single-line container, and the last member of sublist is |
| 6678 // not a br, remove the first member of node list and |
| 6679 // append it to sublist." |
| 6680 while (nodeList.length |
| 6681 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
| 6682 && !isSingleLineContainer(nodeList[0]) |
| 6683 && !isHtmlElement(sublist[sublist.length - 1], "BR")) { |
| 6684 sublist.push(nodeList.shift()); |
| 6685 } |
| 6686 } |
| 6687 |
| 6688 // "Wrap sublist. If value is "div" or "p", sibling criteria |
| 6689 // returns false; otherwise it returns true for an HTML element |
| 6690 // with local name value and no attributes, and false otherwise. |
| 6691 // New parent instructions return the result of running |
| 6692 // createElement(value) on the context object. Then fix disallowed |
| 6693 // ancestors of the result." |
| 6694 fixDisallowedAncestors(wrap(sublist, |
| 6695 ["div", "p"].indexOf(value) == - 1 |
| 6696 ? function(node) { return isHtmlElement(node, value) && !nod
e.attributes.length } |
| 6697 : function() { return false }, |
| 6698 function() { return document.createElement(value) })); |
| 6699 } |
| 6700 |
| 6701 // "Return true." |
| 6702 return true; |
| 6703 }, indeterm: function() { |
| 6704 // "If the active range is null, return false." |
| 6705 if (!getActiveRange()) { |
| 6706 return false; |
| 6707 } |
| 6708 |
| 6709 // "Block-extend the active range, and let new range be the result." |
| 6710 var newRange = blockExtend(getActiveRange()); |
| 6711 |
| 6712 // "Let node list be all visible editable nodes that are contained in |
| 6713 // new range and have no children." |
| 6714 var nodeList = getAllContainedNodes(newRange, function(node) { |
| 6715 return isVisible(node) |
| 6716 && isEditable(node) |
| 6717 && !node.hasChildNodes(); |
| 6718 }); |
| 6719 |
| 6720 // "If node list is empty, return false." |
| 6721 if (!nodeList.length) { |
| 6722 return false; |
| 6723 } |
| 6724 |
| 6725 // "Let type be null." |
| 6726 var type = null; |
| 6727 |
| 6728 // "For each node in node list:" |
| 6729 for (var i = 0; i < nodeList.length; i++) { |
| 6730 var node = nodeList[i]; |
| 6731 |
| 6732 // "While node's parent is editable and in the same editing host as |
| 6733 // node, and node is not an HTML element whose local name is a |
| 6734 // formattable block name, set node to its parent." |
| 6735 while (isEditable(node.parentNode) |
| 6736 && inSameEditingHost(node, node.parentNode) |
| 6737 && !isHtmlElement(node, formattableBlockNames)) { |
| 6738 node = node.parentNode; |
| 6739 } |
| 6740 |
| 6741 // "Let current type be the empty string." |
| 6742 var currentType = ""; |
| 6743 |
| 6744 // "If node is an editable HTML element whose local name is a |
| 6745 // formattable block name, and node is not the ancestor of a |
| 6746 // prohibited paragraph child, set current type to node's local |
| 6747 // name." |
| 6748 if (isEditable(node) |
| 6749 && isHtmlElement(node, formattableBlockNames) |
| 6750 && !getDescendants(node).some(isProhibitedParagraphChild)) { |
| 6751 currentType = node.tagName; |
| 6752 } |
| 6753 |
| 6754 // "If type is null, set type to current type." |
| 6755 if (type === null) { |
| 6756 type = currentType; |
| 6757 |
| 6758 // "Otherwise, if type does not equal current type, return true." |
| 6759 } else if (type != currentType) { |
| 6760 return true; |
| 6761 } |
| 6762 } |
| 6763 |
| 6764 // "Return false." |
| 6765 return false; |
| 6766 }, value: function() { |
| 6767 // "If the active range is null, return the empty string." |
| 6768 if (!getActiveRange()) { |
| 6769 return ""; |
| 6770 } |
| 6771 |
| 6772 // "Block-extend the active range, and let new range be the result." |
| 6773 var newRange = blockExtend(getActiveRange()); |
| 6774 |
| 6775 // "Let node be the first visible editable node that is contained in |
| 6776 // new range and has no children. If there is no such node, return the |
| 6777 // empty string." |
| 6778 var nodes = getAllContainedNodes(newRange, function(node) { |
| 6779 return isVisible(node) |
| 6780 && isEditable(node) |
| 6781 && !node.hasChildNodes(); |
| 6782 }); |
| 6783 if (!nodes.length) { |
| 6784 return ""; |
| 6785 } |
| 6786 var node = nodes[0]; |
| 6787 |
| 6788 // "While node's parent is editable and in the same editing host as |
| 6789 // node, and node is not an HTML element whose local name is a |
| 6790 // formattable block name, set node to its parent." |
| 6791 while (isEditable(node.parentNode) |
| 6792 && inSameEditingHost(node, node.parentNode) |
| 6793 && !isHtmlElement(node, formattableBlockNames)) { |
| 6794 node = node.parentNode; |
| 6795 } |
| 6796 |
| 6797 // "If node is an editable HTML element whose local name is a |
| 6798 // formattable block name, and node is not the ancestor of a prohibited |
| 6799 // paragraph child, return node's local name, converted to ASCII |
| 6800 // lowercase." |
| 6801 if (isEditable(node) |
| 6802 && isHtmlElement(node, formattableBlockNames) |
| 6803 && !getDescendants(node).some(isProhibitedParagraphChild)) { |
| 6804 return node.tagName.toLowerCase(); |
| 6805 } |
| 6806 |
| 6807 // "Return the empty string." |
| 6808 return ""; |
| 6809 } |
| 6810 }; |
| 6811 |
| 6812 //@} |
| 6813 ///// The forwardDelete command ///// |
| 6814 //@{ |
| 6815 commands.forwarddelete = { |
| 6816 preservesOverrides: true, |
| 6817 action: function() { |
| 6818 // "If the active range is not collapsed, delete the selection and |
| 6819 // return true." |
| 6820 if (!getActiveRange().collapsed) { |
| 6821 deleteSelection(); |
| 6822 return true; |
| 6823 } |
| 6824 |
| 6825 // "Canonicalize whitespace at the active range's start." |
| 6826 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange()
.startOffset); |
| 6827 |
| 6828 // "Let node and offset be the active range's start node and offset." |
| 6829 var node = getActiveRange().startContainer; |
| 6830 var offset = getActiveRange().startOffset; |
| 6831 |
| 6832 // "Repeat the following steps:" |
| 6833 while (true) { |
| 6834 // "If offset is the length of node and node's nextSibling is an |
| 6835 // editable invisible node, remove node's nextSibling from its |
| 6836 // parent." |
| 6837 if (offset == getNodeLength(node) |
| 6838 && isEditable(node.nextSibling) |
| 6839 && isInvisible(node.nextSibling)) { |
| 6840 node.parentNode.removeChild(node.nextSibling); |
| 6841 |
| 6842 // "Otherwise, if node has a child with index offset and that child |
| 6843 // is an editable invisible node, remove that child from node." |
| 6844 } else if (offset < node.childNodes.length |
| 6845 && isEditable(node.childNodes[offset]) |
| 6846 && isInvisible(node.childNodes[offset])) { |
| 6847 node.removeChild(node.childNodes[offset]); |
| 6848 |
| 6849 // "Otherwise, if offset is the length of node and node is an |
| 6850 // inline node, or if node is invisible, set offset to one plus the |
| 6851 // index of node, then set node to its parent." |
| 6852 } else if ((offset == getNodeLength(node) |
| 6853 && isInlineNode(node)) |
| 6854 || isInvisible(node)) { |
| 6855 offset = 1 + getNodeIndex(node); |
| 6856 node = node.parentNode; |
| 6857 |
| 6858 // "Otherwise, if node has a child with index offset and that child |
| 6859 // is neither a block node nor a br nor an img nor a collapsed |
| 6860 // block prop, set node to that child, then set offset to zero." |
| 6861 } else if (offset < node.childNodes.length |
| 6862 && !isBlockNode(node.childNodes[offset]) |
| 6863 && !isHtmlElement(node.childNodes[offset], ["br", "img"]) |
| 6864 && !isCollapsedBlockProp(node.childNodes[offset])) { |
| 6865 node = node.childNodes[offset]; |
| 6866 offset = 0; |
| 6867 |
| 6868 // "Otherwise, break from this loop." |
| 6869 } else { |
| 6870 break; |
| 6871 } |
| 6872 } |
| 6873 |
| 6874 // "If node is a Text node and offset is not node's length:" |
| 6875 if (node.nodeType == Node.TEXT_NODE |
| 6876 && offset != getNodeLength(node)) { |
| 6877 // "Let end offset be offset plus one." |
| 6878 var endOffset = offset + 1; |
| 6879 |
| 6880 // "While end offset is not node's length and the end offsetth |
| 6881 // element of node's data has general category M when interpreted |
| 6882 // as a Unicode code point, add one to end offset." |
| 6883 // |
| 6884 // TODO: Not even going to try handling anything beyond the most |
| 6885 // basic combining marks, since I couldn't find a good list. I |
| 6886 // special-case a few Hebrew diacritics too to test basic coverage |
| 6887 // of non-Latin stuff. |
| 6888 while (endOffset != node.length |
| 6889 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOf
fset])) { |
| 6890 endOffset++; |
| 6891 } |
| 6892 |
| 6893 // "Call collapse(node, offset) on the context object's Selection." |
| 6894 getSelection().collapse(node, offset); |
| 6895 getActiveRange().setStart(node, offset); |
| 6896 |
| 6897 // "Call extend(node, end offset) on the context object's |
| 6898 // Selection." |
| 6899 getSelection().extend(node, endOffset); |
| 6900 getActiveRange().setEnd(node, endOffset); |
| 6901 |
| 6902 // "Delete the selection." |
| 6903 deleteSelection(); |
| 6904 |
| 6905 // "Return true." |
| 6906 return true; |
| 6907 } |
| 6908 |
| 6909 // "If node is an inline node, return true." |
| 6910 if (isInlineNode(node)) { |
| 6911 return true; |
| 6912 } |
| 6913 |
| 6914 // "If node has a child with index offset and that child is a br or hr |
| 6915 // or img, but is not a collapsed block prop:" |
| 6916 if (offset < node.childNodes.length |
| 6917 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"]) |
| 6918 && !isCollapsedBlockProp(node.childNodes[offset])) { |
| 6919 // "Call collapse(node, offset) on the context object's Selection." |
| 6920 getSelection().collapse(node, offset); |
| 6921 getActiveRange().setStart(node, offset); |
| 6922 |
| 6923 // "Call extend(node, offset + 1) on the context object's |
| 6924 // Selection." |
| 6925 getSelection().extend(node, offset + 1); |
| 6926 getActiveRange().setEnd(node, offset + 1); |
| 6927 |
| 6928 // "Delete the selection." |
| 6929 deleteSelection(); |
| 6930 |
| 6931 // "Return true." |
| 6932 return true; |
| 6933 } |
| 6934 |
| 6935 // "Let end node equal node and let end offset equal offset." |
| 6936 var endNode = node; |
| 6937 var endOffset = offset; |
| 6938 |
| 6939 // "If end node has a child with index end offset, and that child is a |
| 6940 // collapsed block prop, add one to end offset." |
| 6941 if (endOffset < endNode.childNodes.length |
| 6942 && isCollapsedBlockProp(endNode.childNodes[endOffset])) { |
| 6943 endOffset++; |
| 6944 } |
| 6945 |
| 6946 // "Repeat the following steps:" |
| 6947 while (true) { |
| 6948 // "If end offset is the length of end node, set end offset to one |
| 6949 // plus the index of end node and then set end node to its parent." |
| 6950 if (endOffset == getNodeLength(endNode)) { |
| 6951 endOffset = 1 + getNodeIndex(endNode); |
| 6952 endNode = endNode.parentNode; |
| 6953 |
| 6954 // "Otherwise, if end node has a an editable invisible child with |
| 6955 // index end offset, remove it from end node." |
| 6956 } else if (endOffset < endNode.childNodes.length |
| 6957 && isEditable(endNode.childNodes[endOffset]) |
| 6958 && isInvisible(endNode.childNodes[endOffset])) { |
| 6959 endNode.removeChild(endNode.childNodes[endOffset]); |
| 6960 |
| 6961 // "Otherwise, break from this loop." |
| 6962 } else { |
| 6963 break; |
| 6964 } |
| 6965 } |
| 6966 |
| 6967 // "If the child of end node with index end offset minus one is a |
| 6968 // table, return true." |
| 6969 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) { |
| 6970 return true; |
| 6971 } |
| 6972 |
| 6973 // "If the child of end node with index end offset is a table:" |
| 6974 if (isHtmlElement(endNode.childNodes[endOffset], "table")) { |
| 6975 // "Call collapse(end node, end offset) on the context object's |
| 6976 // Selection." |
| 6977 getSelection().collapse(endNode, endOffset); |
| 6978 getActiveRange().setStart(endNode, endOffset); |
| 6979 |
| 6980 // "Call extend(end node, end offset + 1) on the context object's |
| 6981 // Selection." |
| 6982 getSelection().extend(endNode, endOffset + 1); |
| 6983 getActiveRange().setEnd(endNode, endOffset + 1); |
| 6984 |
| 6985 // "Return true." |
| 6986 return true; |
| 6987 } |
| 6988 |
| 6989 // "If offset is the length of node, and the child of end node with |
| 6990 // index end offset is an hr or br:" |
| 6991 if (offset == getNodeLength(node) |
| 6992 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) { |
| 6993 // "Call collapse(end node, end offset) on the context object's |
| 6994 // Selection." |
| 6995 getSelection().collapse(endNode, endOffset); |
| 6996 getActiveRange().setStart(endNode, endOffset); |
| 6997 |
| 6998 // "Call extend(end node, end offset + 1) on the context object's |
| 6999 // Selection." |
| 7000 getSelection().extend(endNode, endOffset + 1); |
| 7001 getActiveRange().setEnd(endNode, endOffset + 1); |
| 7002 |
| 7003 // "Delete the selection." |
| 7004 deleteSelection(); |
| 7005 |
| 7006 // "Call collapse(node, offset) on the Selection." |
| 7007 getSelection().collapse(node, offset); |
| 7008 getActiveRange().setStart(node, offset); |
| 7009 getActiveRange().collapse(true); |
| 7010 |
| 7011 // "Return true." |
| 7012 return true; |
| 7013 } |
| 7014 |
| 7015 // "While end node has a child with index end offset:" |
| 7016 while (endOffset < endNode.childNodes.length) { |
| 7017 // "If end node's child with index end offset is editable and |
| 7018 // invisible, remove it from end node." |
| 7019 if (isEditable(endNode.childNodes[endOffset]) |
| 7020 && isInvisible(endNode.childNodes[endOffset])) { |
| 7021 endNode.removeChild(endNode.childNodes[endOffset]); |
| 7022 |
| 7023 // "Otherwise, set end node to its child with index end offset and |
| 7024 // set end offset to zero." |
| 7025 } else { |
| 7026 endNode = endNode.childNodes[endOffset]; |
| 7027 endOffset = 0; |
| 7028 } |
| 7029 } |
| 7030 |
| 7031 // "Call collapse(node, offset) on the context object's Selection." |
| 7032 getSelection().collapse(node, offset); |
| 7033 getActiveRange().setStart(node, offset); |
| 7034 |
| 7035 // "Call extend(end node, end offset) on the context object's |
| 7036 // Selection." |
| 7037 getSelection().extend(endNode, endOffset); |
| 7038 getActiveRange().setEnd(endNode, endOffset); |
| 7039 |
| 7040 // "Delete the selection." |
| 7041 deleteSelection(); |
| 7042 |
| 7043 // "Return true." |
| 7044 return true; |
| 7045 } |
| 7046 }; |
| 7047 |
| 7048 //@} |
| 7049 ///// The indent command ///// |
| 7050 //@{ |
| 7051 commands.indent = { |
| 7052 preservesOverrides: true, |
| 7053 action: function() { |
| 7054 // "Let items be a list of all lis that are ancestor containers of the |
| 7055 // active range's start and/or end node." |
| 7056 // |
| 7057 // Has to be in tree order, remember! |
| 7058 var items = []; |
| 7059 for (var node = getActiveRange().endContainer; node != getActiveRange().
commonAncestorContainer; node = node.parentNode) { |
| 7060 if (isHtmlElement(node, "LI")) { |
| 7061 items.unshift(node); |
| 7062 } |
| 7063 } |
| 7064 for (var node = getActiveRange().startContainer; node != getActiveRange(
).commonAncestorContainer; node = node.parentNode) { |
| 7065 if (isHtmlElement(node, "LI")) { |
| 7066 items.unshift(node); |
| 7067 } |
| 7068 } |
| 7069 for (var node = getActiveRange().commonAncestorContainer; node; node = n
ode.parentNode) { |
| 7070 if (isHtmlElement(node, "LI")) { |
| 7071 items.unshift(node); |
| 7072 } |
| 7073 } |
| 7074 |
| 7075 // "For each item in items, normalize sublists of item." |
| 7076 for (var i = 0; i < items.length; i++) { |
| 7077 normalizeSublists(items[i]); |
| 7078 } |
| 7079 |
| 7080 // "Block-extend the active range, and let new range be the result." |
| 7081 var newRange = blockExtend(getActiveRange()); |
| 7082 |
| 7083 // "Let node list be a list of nodes, initially empty." |
| 7084 var nodeList = []; |
| 7085 |
| 7086 // "For each node node contained in new range, if node is editable and |
| 7087 // is an allowed child of "div" or "ol" and if the last member of node |
| 7088 // list (if any) is not an ancestor of node, append node to node list." |
| 7089 nodeList = getContainedNodes(newRange, function(node) { |
| 7090 return isEditable(node) |
| 7091 && (isAllowedChild(node, "div") |
| 7092 || isAllowedChild(node, "ol")); |
| 7093 }); |
| 7094 |
| 7095 // "If the first visible member of node list is an li whose parent is |
| 7096 // an ol or ul:" |
| 7097 if (isHtmlElement(nodeList.filter(isVisible)[0], "li") |
| 7098 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])
) { |
| 7099 // "Let sibling be node list's first visible member's |
| 7100 // previousSibling." |
| 7101 var sibling = nodeList.filter(isVisible)[0].previousSibling; |
| 7102 |
| 7103 // "While sibling is invisible, set sibling to its |
| 7104 // previousSibling." |
| 7105 while (isInvisible(sibling)) { |
| 7106 sibling = sibling.previousSibling; |
| 7107 } |
| 7108 |
| 7109 // "If sibling is an li, normalize sublists of sibling." |
| 7110 if (isHtmlElement(sibling, "li")) { |
| 7111 normalizeSublists(sibling); |
| 7112 } |
| 7113 } |
| 7114 |
| 7115 // "While node list is not empty:" |
| 7116 while (nodeList.length) { |
| 7117 // "Let sublist be a list of nodes, initially empty." |
| 7118 var sublist = []; |
| 7119 |
| 7120 // "Remove the first member of node list and append it to sublist." |
| 7121 sublist.push(nodeList.shift()); |
| 7122 |
| 7123 // "While the first member of node list is the nextSibling of the |
| 7124 // last member of sublist, remove the first member of node list and |
| 7125 // append it to sublist." |
| 7126 while (nodeList.length |
| 7127 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
| 7128 sublist.push(nodeList.shift()); |
| 7129 } |
| 7130 |
| 7131 // "Indent sublist." |
| 7132 indentNodes(sublist); |
| 7133 } |
| 7134 |
| 7135 // "Return true." |
| 7136 return true; |
| 7137 } |
| 7138 }; |
| 7139 |
| 7140 //@} |
| 7141 ///// The insertHorizontalRule command ///// |
| 7142 //@{ |
| 7143 commands.inserthorizontalrule = { |
| 7144 preservesOverrides: true, |
| 7145 action: function() { |
| 7146 // "Let start node, start offset, end node, and end offset be the |
| 7147 // active range's start and end nodes and offsets." |
| 7148 var startNode = getActiveRange().startContainer; |
| 7149 var startOffset = getActiveRange().startOffset; |
| 7150 var endNode = getActiveRange().endContainer; |
| 7151 var endOffset = getActiveRange().endOffset; |
| 7152 |
| 7153 // "While start offset is 0 and start node's parent is not null, set |
| 7154 // start offset to start node's index, then set start node to its |
| 7155 // parent." |
| 7156 while (startOffset == 0 |
| 7157 && startNode.parentNode) { |
| 7158 startOffset = getNodeIndex(startNode); |
| 7159 startNode = startNode.parentNode; |
| 7160 } |
| 7161 |
| 7162 // "While end offset is end node's length, and end node's parent is not |
| 7163 // null, set end offset to one plus end node's index, then set end node |
| 7164 // to its parent." |
| 7165 while (endOffset == getNodeLength(endNode) |
| 7166 && endNode.parentNode) { |
| 7167 endOffset = 1 + getNodeIndex(endNode); |
| 7168 endNode = endNode.parentNode; |
| 7169 } |
| 7170 |
| 7171 // "Call collapse(start node, start offset) on the context object's |
| 7172 // Selection." |
| 7173 getSelection().collapse(startNode, startOffset); |
| 7174 getActiveRange().setStart(startNode, startOffset); |
| 7175 |
| 7176 // "Call extend(end node, end offset) on the context object's |
| 7177 // Selection." |
| 7178 getSelection().extend(endNode, endOffset); |
| 7179 getActiveRange().setEnd(endNode, endOffset); |
| 7180 |
| 7181 // "Delete the selection, with block merging false." |
| 7182 deleteSelection({blockMerging: false}); |
| 7183 |
| 7184 // "If the active range's start node is neither editable nor an editing |
| 7185 // host, return true." |
| 7186 if (!isEditable(getActiveRange().startContainer) |
| 7187 && !isEditingHost(getActiveRange().startContainer)) { |
| 7188 return true; |
| 7189 } |
| 7190 |
| 7191 // "If the active range's start node is a Text node and its start |
| 7192 // offset is zero, call collapse() on the context object's Selection, |
| 7193 // with first argument the active range's start node's parent and |
| 7194 // second argument the active range's start node's index." |
| 7195 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 7196 && getActiveRange().startOffset == 0) { |
| 7197 var newNode = getActiveRange().startContainer.parentNode; |
| 7198 var newOffset = getNodeIndex(getActiveRange().startContainer); |
| 7199 getSelection().collapse(newNode, newOffset); |
| 7200 getActiveRange().setStart(newNode, newOffset); |
| 7201 getActiveRange().collapse(true); |
| 7202 } |
| 7203 |
| 7204 // "If the active range's start node is a Text node and its start |
| 7205 // offset is the length of its start node, call collapse() on the |
| 7206 // context object's Selection, with first argument the active range's |
| 7207 // start node's parent, and the second argument one plus the active |
| 7208 // range's start node's index." |
| 7209 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 7210 && getActiveRange().startOffset == getNodeLength(getActiveRange().startC
ontainer)) { |
| 7211 var newNode = getActiveRange().startContainer.parentNode; |
| 7212 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
| 7213 getSelection().collapse(newNode, newOffset); |
| 7214 getActiveRange().setStart(newNode, newOffset); |
| 7215 getActiveRange().collapse(true); |
| 7216 } |
| 7217 |
| 7218 // "Let hr be the result of calling createElement("hr") on the |
| 7219 // context object." |
| 7220 var hr = document.createElement("hr"); |
| 7221 |
| 7222 // "Run insertNode(hr) on the active range." |
| 7223 getActiveRange().insertNode(hr); |
| 7224 |
| 7225 // "Fix disallowed ancestors of hr." |
| 7226 fixDisallowedAncestors(hr); |
| 7227 |
| 7228 // "Run collapse() on the context object's Selection, with first |
| 7229 // argument hr's parent and the second argument equal to one plus hr's |
| 7230 // index." |
| 7231 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr)); |
| 7232 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr)); |
| 7233 getActiveRange().collapse(true); |
| 7234 |
| 7235 // "Return true." |
| 7236 return true; |
| 7237 } |
| 7238 }; |
| 7239 |
| 7240 //@} |
| 7241 ///// The insertHTML command ///// |
| 7242 //@{ |
| 7243 commands.inserthtml = { |
| 7244 preservesOverrides: true, |
| 7245 action: function(value) { |
| 7246 // "Delete the selection." |
| 7247 deleteSelection(); |
| 7248 |
| 7249 // "If the active range's start node is neither editable nor an editing |
| 7250 // host, return true." |
| 7251 if (!isEditable(getActiveRange().startContainer) |
| 7252 && !isEditingHost(getActiveRange().startContainer)) { |
| 7253 return true; |
| 7254 } |
| 7255 |
| 7256 // "Let frag be the result of calling createContextualFragment(value) |
| 7257 // on the active range." |
| 7258 var frag = getActiveRange().createContextualFragment(value); |
| 7259 |
| 7260 // "Let last child be the lastChild of frag." |
| 7261 var lastChild = frag.lastChild; |
| 7262 |
| 7263 // "If last child is null, return true." |
| 7264 if (!lastChild) { |
| 7265 return true; |
| 7266 } |
| 7267 |
| 7268 // "Let descendants be all descendants of frag." |
| 7269 var descendants = getDescendants(frag); |
| 7270 |
| 7271 // "If the active range's start node is a block node:" |
| 7272 if (isBlockNode(getActiveRange().startContainer)) { |
| 7273 // "Let collapsed block props be all editable collapsed block prop |
| 7274 // children of the active range's start node that have index |
| 7275 // greater than or equal to the active range's start offset." |
| 7276 // |
| 7277 // "For each node in collapsed block props, remove node from its |
| 7278 // parent." |
| 7279 [].filter.call(getActiveRange().startContainer.childNodes, function(
node) { |
| 7280 return isEditable(node) |
| 7281 && isCollapsedBlockProp(node) |
| 7282 && getNodeIndex(node) >= getActiveRange().startOffset; |
| 7283 }).forEach(function(node) { |
| 7284 node.parentNode.removeChild(node); |
| 7285 }); |
| 7286 } |
| 7287 |
| 7288 // "Call insertNode(frag) on the active range." |
| 7289 getActiveRange().insertNode(frag); |
| 7290 |
| 7291 // "If the active range's start node is a block node with no visible |
| 7292 // children, call createElement("br") on the context object and append |
| 7293 // the result as the last child of the active range's start node." |
| 7294 if (isBlockNode(getActiveRange().startContainer) |
| 7295 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible))
{ |
| 7296 getActiveRange().startContainer.appendChild(document.createElement("
br")); |
| 7297 } |
| 7298 |
| 7299 // "Call collapse() on the context object's Selection, with last |
| 7300 // child's parent as the first argument and one plus its index as the |
| 7301 // second." |
| 7302 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChi
ld)); |
| 7303 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild
)); |
| 7304 |
| 7305 // "Fix disallowed ancestors of each member of descendants." |
| 7306 for (var i = 0; i < descendants.length; i++) { |
| 7307 fixDisallowedAncestors(descendants[i]); |
| 7308 } |
| 7309 |
| 7310 // "Return true." |
| 7311 return true; |
| 7312 } |
| 7313 }; |
| 7314 |
| 7315 //@} |
| 7316 ///// The insertImage command ///// |
| 7317 //@{ |
| 7318 commands.insertimage = { |
| 7319 preservesOverrides: true, |
| 7320 action: function(value) { |
| 7321 // "If value is the empty string, return false." |
| 7322 if (value === "") { |
| 7323 return false; |
| 7324 } |
| 7325 |
| 7326 // "Delete the selection, with strip wrappers false." |
| 7327 deleteSelection({stripWrappers: false}); |
| 7328 |
| 7329 // "Let range be the active range." |
| 7330 var range = getActiveRange(); |
| 7331 |
| 7332 // "If the active range's start node is neither editable nor an editing |
| 7333 // host, return true." |
| 7334 if (!isEditable(getActiveRange().startContainer) |
| 7335 && !isEditingHost(getActiveRange().startContainer)) { |
| 7336 return true; |
| 7337 } |
| 7338 |
| 7339 // "If range's start node is a block node whose sole child is a br, and |
| 7340 // its start offset is 0, remove its start node's child from it." |
| 7341 if (isBlockNode(range.startContainer) |
| 7342 && range.startContainer.childNodes.length == 1 |
| 7343 && isHtmlElement(range.startContainer.firstChild, "br") |
| 7344 && range.startOffset == 0) { |
| 7345 range.startContainer.removeChild(range.startContainer.firstChild); |
| 7346 } |
| 7347 |
| 7348 // "Let img be the result of calling createElement("img") on the |
| 7349 // context object." |
| 7350 var img = document.createElement("img"); |
| 7351 |
| 7352 // "Run setAttribute("src", value) on img." |
| 7353 img.setAttribute("src", value); |
| 7354 |
| 7355 // "Run insertNode(img) on the range." |
| 7356 range.insertNode(img); |
| 7357 |
| 7358 // "Run collapse() on the Selection, with first argument equal to the |
| 7359 // parent of img and the second argument equal to one plus the index of |
| 7360 // img." |
| 7361 // |
| 7362 // Not everyone actually supports collapse(), so we do it manually |
| 7363 // instead. Also, we need to modify the actual range we're given as |
| 7364 // well, for the sake of autoimplementation.html's range-filling-in. |
| 7365 range.setStart(img.parentNode, 1 + getNodeIndex(img)); |
| 7366 range.setEnd(img.parentNode, 1 + getNodeIndex(img)); |
| 7367 getSelection().removeAllRanges(); |
| 7368 getSelection().addRange(range); |
| 7369 |
| 7370 // IE adds width and height attributes for some reason, so remove those |
| 7371 // to actually do what the spec says. |
| 7372 img.removeAttribute("width"); |
| 7373 img.removeAttribute("height"); |
| 7374 |
| 7375 // "Return true." |
| 7376 return true; |
| 7377 } |
| 7378 }; |
| 7379 |
| 7380 //@} |
| 7381 ///// The insertLineBreak command ///// |
| 7382 //@{ |
| 7383 commands.insertlinebreak = { |
| 7384 preservesOverrides: true, |
| 7385 action: function(value) { |
| 7386 // "Delete the selection, with strip wrappers false." |
| 7387 deleteSelection({stripWrappers: false}); |
| 7388 |
| 7389 // "If the active range's start node is neither editable nor an editing |
| 7390 // host, return true." |
| 7391 if (!isEditable(getActiveRange().startContainer) |
| 7392 && !isEditingHost(getActiveRange().startContainer)) { |
| 7393 return true; |
| 7394 } |
| 7395 |
| 7396 // "If the active range's start node is an Element, and "br" is not an |
| 7397 // allowed child of it, return true." |
| 7398 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE |
| 7399 && !isAllowedChild("br", getActiveRange().startContainer)) { |
| 7400 return true; |
| 7401 } |
| 7402 |
| 7403 // "If the active range's start node is not an Element, and "br" is not |
| 7404 // an allowed child of the active range's start node's parent, return |
| 7405 // true." |
| 7406 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE |
| 7407 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) { |
| 7408 return true; |
| 7409 } |
| 7410 |
| 7411 // "If the active range's start node is a Text node and its start |
| 7412 // offset is zero, call collapse() on the context object's Selection, |
| 7413 // with first argument equal to the active range's start node's parent |
| 7414 // and second argument equal to the active range's start node's index." |
| 7415 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 7416 && getActiveRange().startOffset == 0) { |
| 7417 var newNode = getActiveRange().startContainer.parentNode; |
| 7418 var newOffset = getNodeIndex(getActiveRange().startContainer); |
| 7419 getSelection().collapse(newNode, newOffset); |
| 7420 getActiveRange().setStart(newNode, newOffset); |
| 7421 getActiveRange().setEnd(newNode, newOffset); |
| 7422 } |
| 7423 |
| 7424 // "If the active range's start node is a Text node and its start |
| 7425 // offset is the length of its start node, call collapse() on the |
| 7426 // context object's Selection, with first argument equal to the active |
| 7427 // range's start node's parent and second argument equal to one plus |
| 7428 // the active range's start node's index." |
| 7429 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
| 7430 && getActiveRange().startOffset == getNodeLength(getActiveRange().startC
ontainer)) { |
| 7431 var newNode = getActiveRange().startContainer.parentNode; |
| 7432 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
| 7433 getSelection().collapse(newNode, newOffset); |
| 7434 getActiveRange().setStart(newNode, newOffset); |
| 7435 getActiveRange().setEnd(newNode, newOffset); |
| 7436 } |
| 7437 |
| 7438 // "Let br be the result of calling createElement("br") on the context |
| 7439 // object." |
| 7440 var br = document.createElement("br"); |
| 7441 |
| 7442 // "Call insertNode(br) on the active range." |
| 7443 getActiveRange().insertNode(br); |
| 7444 |
| 7445 // "Call collapse() on the context object's Selection, with br's parent |
| 7446 // as the first argument and one plus br's index as the second |
| 7447 // argument." |
| 7448 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
| 7449 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
| 7450 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
| 7451 |
| 7452 // "If br is a collapsed line break, call createElement("br") on the |
| 7453 // context object and let extra br be the result, then call |
| 7454 // insertNode(extra br) on the active range." |
| 7455 if (isCollapsedLineBreak(br)) { |
| 7456 getActiveRange().insertNode(document.createElement("br")); |
| 7457 |
| 7458 // Compensate for nonstandard implementations of insertNode |
| 7459 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
| 7460 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
| 7461 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
| 7462 } |
| 7463 |
| 7464 // "Return true." |
| 7465 return true; |
| 7466 } |
| 7467 }; |
| 7468 |
| 7469 //@} |
| 7470 ///// The insertOrderedList command ///// |
| 7471 //@{ |
| 7472 commands.insertorderedlist = { |
| 7473 preservesOverrides: true, |
| 7474 // "Toggle lists with tag name "ol", then return true." |
| 7475 action: function() { toggleLists("ol"); return true }, |
| 7476 // "True if the selection's list state is "mixed" or "mixed ol", false |
| 7477 // otherwise." |
| 7478 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState())
}, |
| 7479 // "True if the selection's list state is "ol", false otherwise." |
| 7480 state: function() { return getSelectionListState() == "ol" }, |
| 7481 }; |
| 7482 |
| 7483 //@} |
| 7484 ///// The insertParagraph command ///// |
| 7485 //@{ |
| 7486 commands.insertparagraph = { |
| 7487 preservesOverrides: true, |
| 7488 action: function() { |
| 7489 // "Delete the selection." |
| 7490 deleteSelection(); |
| 7491 |
| 7492 // "If the active range's start node is neither editable nor an editing |
| 7493 // host, return true." |
| 7494 if (!isEditable(getActiveRange().startContainer) |
| 7495 && !isEditingHost(getActiveRange().startContainer)) { |
| 7496 return true; |
| 7497 } |
| 7498 |
| 7499 // "Let node and offset be the active range's start node and offset." |
| 7500 var node = getActiveRange().startContainer; |
| 7501 var offset = getActiveRange().startOffset; |
| 7502 |
| 7503 // "If node is a Text node, and offset is neither 0 nor the length of |
| 7504 // node, call splitText(offset) on node." |
| 7505 if (node.nodeType == Node.TEXT_NODE |
| 7506 && offset != 0 |
| 7507 && offset != getNodeLength(node)) { |
| 7508 node.splitText(offset); |
| 7509 } |
| 7510 |
| 7511 // "If node is a Text node and offset is its length, set offset to one |
| 7512 // plus the index of node, then set node to its parent." |
| 7513 if (node.nodeType == Node.TEXT_NODE |
| 7514 && offset == getNodeLength(node)) { |
| 7515 offset = 1 + getNodeIndex(node); |
| 7516 node = node.parentNode; |
| 7517 } |
| 7518 |
| 7519 // "If node is a Text or Comment node, set offset to the index of node, |
| 7520 // then set node to its parent." |
| 7521 if (node.nodeType == Node.TEXT_NODE |
| 7522 || node.nodeType == Node.COMMENT_NODE) { |
| 7523 offset = getNodeIndex(node); |
| 7524 node = node.parentNode; |
| 7525 } |
| 7526 |
| 7527 // "Call collapse(node, offset) on the context object's Selection." |
| 7528 getSelection().collapse(node, offset); |
| 7529 getActiveRange().setStart(node, offset); |
| 7530 getActiveRange().setEnd(node, offset); |
| 7531 |
| 7532 // "Let container equal node." |
| 7533 var container = node; |
| 7534 |
| 7535 // "While container is not a single-line container, and container's |
| 7536 // parent is editable and in the same editing host as node, set |
| 7537 // container to its parent." |
| 7538 while (!isSingleLineContainer(container) |
| 7539 && isEditable(container.parentNode) |
| 7540 && inSameEditingHost(node, container.parentNode)) { |
| 7541 container = container.parentNode; |
| 7542 } |
| 7543 |
| 7544 // "If container is an editable single-line container in the same |
| 7545 // editing host as node, and its local name is "p" or "div":" |
| 7546 if (isEditable(container) |
| 7547 && isSingleLineContainer(container) |
| 7548 && inSameEditingHost(node, container.parentNode) |
| 7549 && (container.tagName == "P" || container.tagName == "DIV")) { |
| 7550 // "Let outer container equal container." |
| 7551 var outerContainer = container; |
| 7552 |
| 7553 // "While outer container is not a dd or dt or li, and outer |
| 7554 // container's parent is editable, set outer container to its |
| 7555 // parent." |
| 7556 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"]) |
| 7557 && isEditable(outerContainer.parentNode)) { |
| 7558 outerContainer = outerContainer.parentNode; |
| 7559 } |
| 7560 |
| 7561 // "If outer container is a dd or dt or li, set container to outer |
| 7562 // container." |
| 7563 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) { |
| 7564 container = outerContainer; |
| 7565 } |
| 7566 } |
| 7567 |
| 7568 // "If container is not editable or not in the same editing host as |
| 7569 // node or is not a single-line container:" |
| 7570 if (!isEditable(container) |
| 7571 || !inSameEditingHost(container, node) |
| 7572 || !isSingleLineContainer(container)) { |
| 7573 // "Let tag be the default single-line container name." |
| 7574 var tag = defaultSingleLineContainerName; |
| 7575 |
| 7576 // "Block-extend the active range, and let new range be the |
| 7577 // result." |
| 7578 var newRange = blockExtend(getActiveRange()); |
| 7579 |
| 7580 // "Let node list be a list of nodes, initially empty." |
| 7581 // |
| 7582 // "Append to node list the first node in tree order that is |
| 7583 // contained in new range and is an allowed child of "p", if any." |
| 7584 var nodeList = getContainedNodes(newRange, function(node) { return i
sAllowedChild(node, "p") }) |
| 7585 .slice(0, 1); |
| 7586 |
| 7587 // "If node list is empty:" |
| 7588 if (!nodeList.length) { |
| 7589 // "If tag is not an allowed child of the active range's start |
| 7590 // node, return true." |
| 7591 if (!isAllowedChild(tag, getActiveRange().startContainer)) { |
| 7592 return true; |
| 7593 } |
| 7594 |
| 7595 // "Set container to the result of calling createElement(tag) |
| 7596 // on the context object." |
| 7597 container = document.createElement(tag); |
| 7598 |
| 7599 // "Call insertNode(container) on the active range." |
| 7600 getActiveRange().insertNode(container); |
| 7601 |
| 7602 // "Call createElement("br") on the context object, and append |
| 7603 // the result as the last child of container." |
| 7604 container.appendChild(document.createElement("br")); |
| 7605 |
| 7606 // "Call collapse(container, 0) on the context object's |
| 7607 // Selection." |
| 7608 getSelection().collapse(container, 0); |
| 7609 getActiveRange().setStart(container, 0); |
| 7610 getActiveRange().setEnd(container, 0); |
| 7611 |
| 7612 // "Return true." |
| 7613 return true; |
| 7614 } |
| 7615 |
| 7616 // "While the nextSibling of the last member of node list is not |
| 7617 // null and is an allowed child of "p", append it to node list." |
| 7618 while (nodeList[nodeList.length - 1].nextSibling |
| 7619 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { |
| 7620 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
| 7621 } |
| 7622 |
| 7623 // "Wrap node list, with sibling criteria returning false and new |
| 7624 // parent instructions returning the result of calling |
| 7625 // createElement(tag) on the context object. Set container to the |
| 7626 // result." |
| 7627 container = wrap(nodeList, |
| 7628 function() { return false }, |
| 7629 function() { return document.createElement(tag) } |
| 7630 ); |
| 7631 } |
| 7632 |
| 7633 // "If container's local name is "address", "listing", or "pre":" |
| 7634 if (container.tagName == "ADDRESS" |
| 7635 || container.tagName == "LISTING" |
| 7636 || container.tagName == "PRE") { |
| 7637 // "Let br be the result of calling createElement("br") on the |
| 7638 // context object." |
| 7639 var br = document.createElement("br"); |
| 7640 |
| 7641 // "Call insertNode(br) on the active range." |
| 7642 getActiveRange().insertNode(br); |
| 7643 |
| 7644 // "Call collapse(node, offset + 1) on the context object's |
| 7645 // Selection." |
| 7646 getSelection().collapse(node, offset + 1); |
| 7647 getActiveRange().setStart(node, offset + 1); |
| 7648 getActiveRange().setEnd(node, offset + 1); |
| 7649 |
| 7650 // "If br is the last descendant of container, let br be the result |
| 7651 // of calling createElement("br") on the context object, then call |
| 7652 // insertNode(br) on the active range." |
| 7653 // |
| 7654 // Work around browser bugs: some browsers select the |
| 7655 // newly-inserted node, not per spec. |
| 7656 if (!isDescendant(nextNode(br), container)) { |
| 7657 getActiveRange().insertNode(document.createElement("br")); |
| 7658 getSelection().collapse(node, offset + 1); |
| 7659 getActiveRange().setEnd(node, offset + 1); |
| 7660 } |
| 7661 |
| 7662 // "Return true." |
| 7663 return true; |
| 7664 } |
| 7665 |
| 7666 // "If container's local name is "li", "dt", or "dd"; and either it has |
| 7667 // no children or it has a single child and that child is a br:" |
| 7668 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1 |
| 7669 && (!container.hasChildNodes() |
| 7670 || (container.childNodes.length == 1 |
| 7671 && isHtmlElement(container.firstChild, "br")))) { |
| 7672 // "Split the parent of the one-node list consisting of container." |
| 7673 splitParent([container]); |
| 7674 |
| 7675 // "If container has no children, call createElement("br") on the |
| 7676 // context object and append the result as the last child of |
| 7677 // container." |
| 7678 if (!container.hasChildNodes()) { |
| 7679 container.appendChild(document.createElement("br")); |
| 7680 } |
| 7681 |
| 7682 // "If container is a dd or dt, and it is not an allowed child of |
| 7683 // any of its ancestors in the same editing host, set the tag name |
| 7684 // of container to the default single-line container name and let |
| 7685 // container be the result." |
| 7686 if (isHtmlElement(container, ["dd", "dt"]) |
| 7687 && getAncestors(container).every(function(ancestor) { |
| 7688 return !inSameEditingHost(container, ancestor) |
| 7689 || !isAllowedChild(container, ancestor) |
| 7690 })) { |
| 7691 container = setTagName(container, defaultSingleLineContainerName
); |
| 7692 } |
| 7693 |
| 7694 // "Fix disallowed ancestors of container." |
| 7695 fixDisallowedAncestors(container); |
| 7696 |
| 7697 // "Return true." |
| 7698 return true; |
| 7699 } |
| 7700 |
| 7701 // "Let new line range be a new range whose start is the same as |
| 7702 // the active range's, and whose end is (container, length of |
| 7703 // container)." |
| 7704 var newLineRange = document.createRange(); |
| 7705 newLineRange.setStart(getActiveRange().startContainer, getActiveRange().
startOffset); |
| 7706 newLineRange.setEnd(container, getNodeLength(container)); |
| 7707 |
| 7708 // "While new line range's start offset is zero and its start node is |
| 7709 // not a prohibited paragraph child, set its start to (parent of start |
| 7710 // node, index of start node)." |
| 7711 while (newLineRange.startOffset == 0 |
| 7712 && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
| 7713 newLineRange.setStart(newLineRange.startContainer.parentNode, getNod
eIndex(newLineRange.startContainer)); |
| 7714 } |
| 7715 |
| 7716 // "While new line range's start offset is the length of its start node |
| 7717 // and its start node is not a prohibited paragraph child, set its |
| 7718 // start to (parent of start node, 1 + index of start node)." |
| 7719 while (newLineRange.startOffset == getNodeLength(newLineRange.startConta
iner) |
| 7720 && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
| 7721 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + ge
tNodeIndex(newLineRange.startContainer)); |
| 7722 } |
| 7723 |
| 7724 // "Let end of line be true if new line range contains either nothing |
| 7725 // or a single br, and false otherwise." |
| 7726 var containedInNewLineRange = getContainedNodes(newLineRange); |
| 7727 var endOfLine = !containedInNewLineRange.length |
| 7728 || (containedInNewLineRange.length == 1 |
| 7729 && isHtmlElement(containedInNewLineRange[0], "br")); |
| 7730 |
| 7731 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or |
| 7732 // "h6", and end of line is true, let new container name be the default |
| 7733 // single-line container name." |
| 7734 var newContainerName; |
| 7735 if (/^H[1-6]$/.test(container.tagName) |
| 7736 && endOfLine) { |
| 7737 newContainerName = defaultSingleLineContainerName; |
| 7738 |
| 7739 // "Otherwise, if the local name of container is "dt" and end of line |
| 7740 // is true, let new container name be "dd"." |
| 7741 } else if (container.tagName == "DT" |
| 7742 && endOfLine) { |
| 7743 newContainerName = "dd"; |
| 7744 |
| 7745 // "Otherwise, if the local name of container is "dd" and end of line |
| 7746 // is true, let new container name be "dt"." |
| 7747 } else if (container.tagName == "DD" |
| 7748 && endOfLine) { |
| 7749 newContainerName = "dt"; |
| 7750 |
| 7751 // "Otherwise, let new container name be the local name of container." |
| 7752 } else { |
| 7753 newContainerName = container.tagName.toLowerCase(); |
| 7754 } |
| 7755 |
| 7756 // "Let new container be the result of calling createElement(new |
| 7757 // container name) on the context object." |
| 7758 var newContainer = document.createElement(newContainerName); |
| 7759 |
| 7760 // "Copy all attributes of container to new container." |
| 7761 for (var i = 0; i < container.attributes.length; i++) { |
| 7762 newContainer.setAttributeNS(container.attributes[i].namespaceURI, co
ntainer.attributes[i].name, container.attributes[i].value); |
| 7763 } |
| 7764 |
| 7765 // "If new container has an id attribute, unset it." |
| 7766 newContainer.removeAttribute("id"); |
| 7767 |
| 7768 // "Insert new container into the parent of container immediately after |
| 7769 // container." |
| 7770 container.parentNode.insertBefore(newContainer, container.nextSibling); |
| 7771 |
| 7772 // "Let contained nodes be all nodes contained in new line range." |
| 7773 var containedNodes = getAllContainedNodes(newLineRange); |
| 7774 |
| 7775 // "Let frag be the result of calling extractContents() on new line |
| 7776 // range." |
| 7777 var frag = newLineRange.extractContents(); |
| 7778 |
| 7779 // "Unset the id attribute (if any) of each Element descendant of frag |
| 7780 // that is not in contained nodes." |
| 7781 var descendants = getDescendants(frag); |
| 7782 for (var i = 0; i < descendants.length; i++) { |
| 7783 if (descendants[i].nodeType == Node.ELEMENT_NODE |
| 7784 && containedNodes.indexOf(descendants[i]) == -1) { |
| 7785 descendants[i].removeAttribute("id"); |
| 7786 } |
| 7787 } |
| 7788 |
| 7789 // "Call appendChild(frag) on new container." |
| 7790 newContainer.appendChild(frag); |
| 7791 |
| 7792 // "While container's lastChild is a prohibited paragraph child, set |
| 7793 // container to its lastChild." |
| 7794 while (isProhibitedParagraphChild(container.lastChild)) { |
| 7795 container = container.lastChild; |
| 7796 } |
| 7797 |
| 7798 // "While new container's lastChild is a prohibited paragraph child, |
| 7799 // set new container to its lastChild." |
| 7800 while (isProhibitedParagraphChild(newContainer.lastChild)) { |
| 7801 newContainer = newContainer.lastChild; |
| 7802 } |
| 7803 |
| 7804 // "If container has no visible children, call createElement("br") on |
| 7805 // the context object, and append the result as the last child of |
| 7806 // container." |
| 7807 if (![].some.call(container.childNodes, isVisible)) { |
| 7808 container.appendChild(document.createElement("br")); |
| 7809 } |
| 7810 |
| 7811 // "If new container has no visible children, call createElement("br") |
| 7812 // on the context object, and append the result as the last child of |
| 7813 // new container." |
| 7814 if (![].some.call(newContainer.childNodes, isVisible)) { |
| 7815 newContainer.appendChild(document.createElement("br")); |
| 7816 } |
| 7817 |
| 7818 // "Call collapse(new container, 0) on the context object's Selection." |
| 7819 getSelection().collapse(newContainer, 0); |
| 7820 getActiveRange().setStart(newContainer, 0); |
| 7821 getActiveRange().setEnd(newContainer, 0); |
| 7822 |
| 7823 // "Return true." |
| 7824 return true; |
| 7825 } |
| 7826 }; |
| 7827 |
| 7828 //@} |
| 7829 ///// The insertText command ///// |
| 7830 //@{ |
| 7831 commands.inserttext = { |
| 7832 action: function(value) { |
| 7833 // "Delete the selection, with strip wrappers false." |
| 7834 deleteSelection({stripWrappers: false}); |
| 7835 |
| 7836 // "If the active range's start node is neither editable nor an editing |
| 7837 // host, return true." |
| 7838 if (!isEditable(getActiveRange().startContainer) |
| 7839 && !isEditingHost(getActiveRange().startContainer)) { |
| 7840 return true; |
| 7841 } |
| 7842 |
| 7843 // "If value's length is greater than one:" |
| 7844 if (value.length > 1) { |
| 7845 // "For each element el in value, take the action for the |
| 7846 // insertText command, with value equal to el." |
| 7847 for (var i = 0; i < value.length; i++) { |
| 7848 commands.inserttext.action(value[i]); |
| 7849 } |
| 7850 |
| 7851 // "Return true." |
| 7852 return true; |
| 7853 } |
| 7854 |
| 7855 // "If value is the empty string, return true." |
| 7856 if (value == "") { |
| 7857 return true; |
| 7858 } |
| 7859 |
| 7860 // "If value is a newline (U+00A0), take the action for the |
| 7861 // insertParagraph command and return true." |
| 7862 if (value == "\n") { |
| 7863 commands.insertparagraph.action(); |
| 7864 return true; |
| 7865 } |
| 7866 |
| 7867 // "Let node and offset be the active range's start node and offset." |
| 7868 var node = getActiveRange().startContainer; |
| 7869 var offset = getActiveRange().startOffset; |
| 7870 |
| 7871 // "If node has a child whose index is offset − 1, and that child is a |
| 7872 // Text node, set node to that child, then set offset to node's |
| 7873 // length." |
| 7874 if (0 <= offset - 1 |
| 7875 && offset - 1 < node.childNodes.length |
| 7876 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) { |
| 7877 node = node.childNodes[offset - 1]; |
| 7878 offset = getNodeLength(node); |
| 7879 } |
| 7880 |
| 7881 // "If node has a child whose index is offset, and that child is a Text |
| 7882 // node, set node to that child, then set offset to zero." |
| 7883 if (0 <= offset |
| 7884 && offset < node.childNodes.length |
| 7885 && node.childNodes[offset].nodeType == Node.TEXT_NODE) { |
| 7886 node = node.childNodes[offset]; |
| 7887 offset = 0; |
| 7888 } |
| 7889 |
| 7890 // "Record current overrides, and let overrides be the result." |
| 7891 var overrides = recordCurrentOverrides(); |
| 7892 |
| 7893 // "Call collapse(node, offset) on the context object's Selection." |
| 7894 getSelection().collapse(node, offset); |
| 7895 getActiveRange().setStart(node, offset); |
| 7896 getActiveRange().setEnd(node, offset); |
| 7897 |
| 7898 // "Canonicalize whitespace at (node, offset)." |
| 7899 canonicalizeWhitespace(node, offset); |
| 7900 |
| 7901 // "Let (node, offset) be the active range's start." |
| 7902 node = getActiveRange().startContainer; |
| 7903 offset = getActiveRange().startOffset; |
| 7904 |
| 7905 // "If node is a Text node:" |
| 7906 if (node.nodeType == Node.TEXT_NODE) { |
| 7907 // "Call insertData(offset, value) on node." |
| 7908 node.insertData(offset, value); |
| 7909 |
| 7910 // "Call collapse(node, offset) on the context object's Selection." |
| 7911 getSelection().collapse(node, offset); |
| 7912 getActiveRange().setStart(node, offset); |
| 7913 |
| 7914 // "Call extend(node, offset + 1) on the context object's |
| 7915 // Selection." |
| 7916 // |
| 7917 // Work around WebKit bug: the extend() can throw if the text we're |
| 7918 // adding is trailing whitespace. |
| 7919 try { getSelection().extend(node, offset + 1); } catch(e) {} |
| 7920 getActiveRange().setEnd(node, offset + 1); |
| 7921 |
| 7922 // "Otherwise:" |
| 7923 } else { |
| 7924 // "If node has only one child, which is a collapsed line break, |
| 7925 // remove its child from it." |
| 7926 // |
| 7927 // FIXME: IE incorrectly returns false here instead of true |
| 7928 // sometimes? |
| 7929 if (node.childNodes.length == 1 |
| 7930 && isCollapsedLineBreak(node.firstChild)) { |
| 7931 node.removeChild(node.firstChild); |
| 7932 } |
| 7933 |
| 7934 // "Let text be the result of calling createTextNode(value) on the |
| 7935 // context object." |
| 7936 var text = document.createTextNode(value); |
| 7937 |
| 7938 // "Call insertNode(text) on the active range." |
| 7939 getActiveRange().insertNode(text); |
| 7940 |
| 7941 // "Call collapse(text, 0) on the context object's Selection." |
| 7942 getSelection().collapse(text, 0); |
| 7943 getActiveRange().setStart(text, 0); |
| 7944 |
| 7945 // "Call extend(text, 1) on the context object's Selection." |
| 7946 getSelection().extend(text, 1); |
| 7947 getActiveRange().setEnd(text, 1); |
| 7948 } |
| 7949 |
| 7950 // "Restore states and values from overrides." |
| 7951 restoreStatesAndValues(overrides); |
| 7952 |
| 7953 // "Canonicalize whitespace at the active range's start, with fix |
| 7954 // collapsed space false." |
| 7955 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange()
.startOffset, false); |
| 7956 |
| 7957 // "Canonicalize whitespace at the active range's end, with fix |
| 7958 // collapsed space false." |
| 7959 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().e
ndOffset, false); |
| 7960 |
| 7961 // "If value is a space character, autolink the active range's start." |
| 7962 if (/^[ \t\n\f\r]$/.test(value)) { |
| 7963 autolink(getActiveRange().startContainer, getActiveRange().startOffs
et); |
| 7964 } |
| 7965 |
| 7966 // "Call collapseToEnd() on the context object's Selection." |
| 7967 // |
| 7968 // Work around WebKit bug: sometimes it blows up the selection and |
| 7969 // throws, which we don't want. |
| 7970 try { getSelection().collapseToEnd(); } catch(e) {} |
| 7971 getActiveRange().collapse(false); |
| 7972 |
| 7973 // "Return true." |
| 7974 return true; |
| 7975 } |
| 7976 }; |
| 7977 |
| 7978 //@} |
| 7979 ///// The insertUnorderedList command ///// |
| 7980 //@{ |
| 7981 commands.insertunorderedlist = { |
| 7982 preservesOverrides: true, |
| 7983 // "Toggle lists with tag name "ul", then return true." |
| 7984 action: function() { toggleLists("ul"); return true }, |
| 7985 // "True if the selection's list state is "mixed" or "mixed ul", false |
| 7986 // otherwise." |
| 7987 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState())
}, |
| 7988 // "True if the selection's list state is "ul", false otherwise." |
| 7989 state: function() { return getSelectionListState() == "ul" }, |
| 7990 }; |
| 7991 |
| 7992 //@} |
| 7993 ///// The justifyCenter command ///// |
| 7994 //@{ |
| 7995 commands.justifycenter = { |
| 7996 preservesOverrides: true, |
| 7997 // "Justify the selection with alignment "center", then return true." |
| 7998 action: function() { justifySelection("center"); return true }, |
| 7999 indeterm: function() { |
| 8000 // "Return false if the active range is null. Otherwise, block-extend |
| 8001 // the active range. Return true if among visible editable nodes that |
| 8002 // are contained in the result and have no children, at least one has |
| 8003 // alignment value "center" and at least one does not. Otherwise return |
| 8004 // false." |
| 8005 if (!getActiveRange()) { |
| 8006 return false; |
| 8007 } |
| 8008 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8009 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8010 }); |
| 8011 return nodes.some(function(node) { return getAlignmentValue(node) == "ce
nter" }) |
| 8012 && nodes.some(function(node) { return getAlignmentValue(node) != "ce
nter" }); |
| 8013 }, state: function() { |
| 8014 // "Return false if the active range is null. Otherwise, block-extend |
| 8015 // the active range. Return true if there is at least one visible |
| 8016 // editable node that is contained in the result and has no children, |
| 8017 // and all such nodes have alignment value "center". Otherwise return |
| 8018 // false." |
| 8019 if (!getActiveRange()) { |
| 8020 return false; |
| 8021 } |
| 8022 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8023 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8024 }); |
| 8025 return nodes.length |
| 8026 && nodes.every(function(node) { return getAlignmentValue(node) == "c
enter" }); |
| 8027 }, value: function() { |
| 8028 // "Return the empty string if the active range is null. Otherwise, |
| 8029 // block-extend the active range, and return the alignment value of the |
| 8030 // first visible editable node that is contained in the result and has |
| 8031 // no children. If there is no such node, return "left"." |
| 8032 if (!getActiveRange()) { |
| 8033 return ""; |
| 8034 } |
| 8035 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8036 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8037 }); |
| 8038 if (nodes.length) { |
| 8039 return getAlignmentValue(nodes[0]); |
| 8040 } else { |
| 8041 return "left"; |
| 8042 } |
| 8043 }, |
| 8044 }; |
| 8045 |
| 8046 //@} |
| 8047 ///// The justifyFull command ///// |
| 8048 //@{ |
| 8049 commands.justifyfull = { |
| 8050 preservesOverrides: true, |
| 8051 // "Justify the selection with alignment "justify", then return true." |
| 8052 action: function() { justifySelection("justify"); return true }, |
| 8053 indeterm: function() { |
| 8054 // "Return false if the active range is null. Otherwise, block-extend |
| 8055 // the active range. Return true if among visible editable nodes that |
| 8056 // are contained in the result and have no children, at least one has |
| 8057 // alignment value "justify" and at least one does not. Otherwise |
| 8058 // return false." |
| 8059 if (!getActiveRange()) { |
| 8060 return false; |
| 8061 } |
| 8062 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8063 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8064 }); |
| 8065 return nodes.some(function(node) { return getAlignmentValue(node) == "ju
stify" }) |
| 8066 && nodes.some(function(node) { return getAlignmentValue(node) != "ju
stify" }); |
| 8067 }, state: function() { |
| 8068 // "Return false if the active range is null. Otherwise, block-extend |
| 8069 // the active range. Return true if there is at least one visible |
| 8070 // editable node that is contained in the result and has no children, |
| 8071 // and all such nodes have alignment value "justify". Otherwise return |
| 8072 // false." |
| 8073 if (!getActiveRange()) { |
| 8074 return false; |
| 8075 } |
| 8076 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8077 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8078 }); |
| 8079 return nodes.length |
| 8080 && nodes.every(function(node) { return getAlignmentValue(node) == "j
ustify" }); |
| 8081 }, value: function() { |
| 8082 // "Return the empty string if the active range is null. Otherwise, |
| 8083 // block-extend the active range, and return the alignment value of the |
| 8084 // first visible editable node that is contained in the result and has |
| 8085 // no children. If there is no such node, return "left"." |
| 8086 if (!getActiveRange()) { |
| 8087 return ""; |
| 8088 } |
| 8089 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8090 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8091 }); |
| 8092 if (nodes.length) { |
| 8093 return getAlignmentValue(nodes[0]); |
| 8094 } else { |
| 8095 return "left"; |
| 8096 } |
| 8097 }, |
| 8098 }; |
| 8099 |
| 8100 //@} |
| 8101 ///// The justifyLeft command ///// |
| 8102 //@{ |
| 8103 commands.justifyleft = { |
| 8104 preservesOverrides: true, |
| 8105 // "Justify the selection with alignment "left", then return true." |
| 8106 action: function() { justifySelection("left"); return true }, |
| 8107 indeterm: function() { |
| 8108 // "Return false if the active range is null. Otherwise, block-extend |
| 8109 // the active range. Return true if among visible editable nodes that |
| 8110 // are contained in the result and have no children, at least one has |
| 8111 // alignment value "left" and at least one does not. Otherwise return |
| 8112 // false." |
| 8113 if (!getActiveRange()) { |
| 8114 return false; |
| 8115 } |
| 8116 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8117 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8118 }); |
| 8119 return nodes.some(function(node) { return getAlignmentValue(node) == "le
ft" }) |
| 8120 && nodes.some(function(node) { return getAlignmentValue(node) != "le
ft" }); |
| 8121 }, state: function() { |
| 8122 // "Return false if the active range is null. Otherwise, block-extend |
| 8123 // the active range. Return true if there is at least one visible |
| 8124 // editable node that is contained in the result and has no children, |
| 8125 // and all such nodes have alignment value "left". Otherwise return |
| 8126 // false." |
| 8127 if (!getActiveRange()) { |
| 8128 return false; |
| 8129 } |
| 8130 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8131 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8132 }); |
| 8133 return nodes.length |
| 8134 && nodes.every(function(node) { return getAlignmentValue(node) == "l
eft" }); |
| 8135 }, value: function() { |
| 8136 // "Return the empty string if the active range is null. Otherwise, |
| 8137 // block-extend the active range, and return the alignment value of the |
| 8138 // first visible editable node that is contained in the result and has |
| 8139 // no children. If there is no such node, return "left"." |
| 8140 if (!getActiveRange()) { |
| 8141 return ""; |
| 8142 } |
| 8143 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8144 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8145 }); |
| 8146 if (nodes.length) { |
| 8147 return getAlignmentValue(nodes[0]); |
| 8148 } else { |
| 8149 return "left"; |
| 8150 } |
| 8151 }, |
| 8152 }; |
| 8153 |
| 8154 //@} |
| 8155 ///// The justifyRight command ///// |
| 8156 //@{ |
| 8157 commands.justifyright = { |
| 8158 preservesOverrides: true, |
| 8159 // "Justify the selection with alignment "right", then return true." |
| 8160 action: function() { justifySelection("right"); return true }, |
| 8161 indeterm: function() { |
| 8162 // "Return false if the active range is null. Otherwise, block-extend |
| 8163 // the active range. Return true if among visible editable nodes that |
| 8164 // are contained in the result and have no children, at least one has |
| 8165 // alignment value "right" and at least one does not. Otherwise return |
| 8166 // false." |
| 8167 if (!getActiveRange()) { |
| 8168 return false; |
| 8169 } |
| 8170 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8171 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8172 }); |
| 8173 return nodes.some(function(node) { return getAlignmentValue(node) == "ri
ght" }) |
| 8174 && nodes.some(function(node) { return getAlignmentValue(node) != "ri
ght" }); |
| 8175 }, state: function() { |
| 8176 // "Return false if the active range is null. Otherwise, block-extend |
| 8177 // the active range. Return true if there is at least one visible |
| 8178 // editable node that is contained in the result and has no children, |
| 8179 // and all such nodes have alignment value "right". Otherwise return |
| 8180 // false." |
| 8181 if (!getActiveRange()) { |
| 8182 return false; |
| 8183 } |
| 8184 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8185 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8186 }); |
| 8187 return nodes.length |
| 8188 && nodes.every(function(node) { return getAlignmentValue(node) == "r
ight" }); |
| 8189 }, value: function() { |
| 8190 // "Return the empty string if the active range is null. Otherwise, |
| 8191 // block-extend the active range, and return the alignment value of the |
| 8192 // first visible editable node that is contained in the result and has |
| 8193 // no children. If there is no such node, return "left"." |
| 8194 if (!getActiveRange()) { |
| 8195 return ""; |
| 8196 } |
| 8197 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function
(node) { |
| 8198 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
| 8199 }); |
| 8200 if (nodes.length) { |
| 8201 return getAlignmentValue(nodes[0]); |
| 8202 } else { |
| 8203 return "left"; |
| 8204 } |
| 8205 }, |
| 8206 }; |
| 8207 |
| 8208 //@} |
| 8209 ///// The outdent command ///// |
| 8210 //@{ |
| 8211 commands.outdent = { |
| 8212 preservesOverrides: true, |
| 8213 action: function() { |
| 8214 // "Let items be a list of all lis that are ancestor containers of the |
| 8215 // range's start and/or end node." |
| 8216 // |
| 8217 // It's annoying to get this in tree order using functional stuff |
| 8218 // without doing getDescendants(document), which is slow, so I do it |
| 8219 // imperatively. |
| 8220 var items = []; |
| 8221 (function(){ |
| 8222 for ( |
| 8223 var ancestorContainer = getActiveRange().endContainer; |
| 8224 ancestorContainer != getActiveRange().commonAncestorContainer; |
| 8225 ancestorContainer = ancestorContainer.parentNode |
| 8226 ) { |
| 8227 if (isHtmlElement(ancestorContainer, "li")) { |
| 8228 items.unshift(ancestorContainer); |
| 8229 } |
| 8230 } |
| 8231 for ( |
| 8232 var ancestorContainer = getActiveRange().startContainer; |
| 8233 ancestorContainer; |
| 8234 ancestorContainer = ancestorContainer.parentNode |
| 8235 ) { |
| 8236 if (isHtmlElement(ancestorContainer, "li")) { |
| 8237 items.unshift(ancestorContainer); |
| 8238 } |
| 8239 } |
| 8240 })(); |
| 8241 |
| 8242 // "For each item in items, normalize sublists of item." |
| 8243 items.forEach(normalizeSublists); |
| 8244 |
| 8245 // "Block-extend the active range, and let new range be the result." |
| 8246 var newRange = blockExtend(getActiveRange()); |
| 8247 |
| 8248 // "Let node list be a list of nodes, initially empty." |
| 8249 // |
| 8250 // "For each node node contained in new range, append node to node list |
| 8251 // if the last member of node list (if any) is not an ancestor of node; |
| 8252 // node is editable; and either node has no editable descendants, or is |
| 8253 // an ol or ul, or is an li whose parent is an ol or ul." |
| 8254 var nodeList = getContainedNodes(newRange, function(node) { |
| 8255 return isEditable(node) |
| 8256 && (!getDescendants(node).some(isEditable) |
| 8257 || isHtmlElement(node, ["ol", "ul"]) |
| 8258 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode,
["ol", "ul"]))); |
| 8259 }); |
| 8260 |
| 8261 // "While node list is not empty:" |
| 8262 while (nodeList.length) { |
| 8263 // "While the first member of node list is an ol or ul or is not |
| 8264 // the child of an ol or ul, outdent it and remove it from node |
| 8265 // list." |
| 8266 while (nodeList.length |
| 8267 && (isHtmlElement(nodeList[0], ["OL", "UL"]) |
| 8268 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) { |
| 8269 outdentNode(nodeList.shift()); |
| 8270 } |
| 8271 |
| 8272 // "If node list is empty, break from these substeps." |
| 8273 if (!nodeList.length) { |
| 8274 break; |
| 8275 } |
| 8276 |
| 8277 // "Let sublist be a list of nodes, initially empty." |
| 8278 var sublist = []; |
| 8279 |
| 8280 // "Remove the first member of node list and append it to sublist." |
| 8281 sublist.push(nodeList.shift()); |
| 8282 |
| 8283 // "While the first member of node list is the nextSibling of the |
| 8284 // last member of sublist, and the first member of node list is not |
| 8285 // an ol or ul, remove the first member of node list and append it |
| 8286 // to sublist." |
| 8287 while (nodeList.length |
| 8288 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
| 8289 && !isHtmlElement(nodeList[0], ["OL", "UL"])) { |
| 8290 sublist.push(nodeList.shift()); |
| 8291 } |
| 8292 |
| 8293 // "Record the values of sublist, and let values be the result." |
| 8294 var values = recordValues(sublist); |
| 8295 |
| 8296 // "Split the parent of sublist, with new parent null." |
| 8297 splitParent(sublist); |
| 8298 |
| 8299 // "Fix disallowed ancestors of each member of sublist." |
| 8300 sublist.forEach(fixDisallowedAncestors); |
| 8301 |
| 8302 // "Restore the values from values." |
| 8303 restoreValues(values); |
| 8304 } |
| 8305 |
| 8306 // "Return true." |
| 8307 return true; |
| 8308 } |
| 8309 }; |
| 8310 |
| 8311 //@} |
| 8312 |
| 8313 ////////////////////////////////// |
| 8314 ///// Miscellaneous commands ///// |
| 8315 ////////////////////////////////// |
| 8316 |
| 8317 ///// The defaultParagraphSeparator command ///// |
| 8318 //@{ |
| 8319 commands.defaultparagraphseparator = { |
| 8320 action: function(value) { |
| 8321 // "Let value be converted to ASCII lowercase. If value is then equal |
| 8322 // to "p" or "div", set the context object's default single-line |
| 8323 // container name to value and return true. Otherwise, return false." |
| 8324 value = value.toLowerCase(); |
| 8325 if (value == "p" || value == "div") { |
| 8326 defaultSingleLineContainerName = value; |
| 8327 return true; |
| 8328 } |
| 8329 return false; |
| 8330 }, value: function() { |
| 8331 // "Return the context object's default single-line container name." |
| 8332 return defaultSingleLineContainerName; |
| 8333 }, |
| 8334 }; |
| 8335 |
| 8336 //@} |
| 8337 ///// The selectAll command ///// |
| 8338 //@{ |
| 8339 commands.selectall = { |
| 8340 // Note, this ignores the whole globalRange/getActiveRange() thing and |
| 8341 // works with actual selections. Not suitable for autoimplementation.html. |
| 8342 action: function() { |
| 8343 // "Let target be the body element of the context object." |
| 8344 var target = document.body; |
| 8345 |
| 8346 // "If target is null, let target be the context object's |
| 8347 // documentElement." |
| 8348 if (!target) { |
| 8349 target = document.documentElement; |
| 8350 } |
| 8351 |
| 8352 // "If target is null, call getSelection() on the context object, and |
| 8353 // call removeAllRanges() on the result." |
| 8354 if (!target) { |
| 8355 getSelection().removeAllRanges(); |
| 8356 |
| 8357 // "Otherwise, call getSelection() on the context object, and call |
| 8358 // selectAllChildren(target) on the result." |
| 8359 } else { |
| 8360 getSelection().selectAllChildren(target); |
| 8361 } |
| 8362 |
| 8363 // "Return true." |
| 8364 return true; |
| 8365 } |
| 8366 }; |
| 8367 |
| 8368 //@} |
| 8369 ///// The styleWithCSS command ///// |
| 8370 //@{ |
| 8371 commands.stylewithcss = { |
| 8372 action: function(value) { |
| 8373 // "If value is an ASCII case-insensitive match for the string |
| 8374 // "false", set the CSS styling flag to false. Otherwise, set the |
| 8375 // CSS styling flag to true. Either way, return true." |
| 8376 cssStylingFlag = String(value).toLowerCase() != "false"; |
| 8377 return true; |
| 8378 }, state: function() { return cssStylingFlag } |
| 8379 }; |
| 8380 |
| 8381 //@} |
| 8382 ///// The useCSS command ///// |
| 8383 //@{ |
| 8384 commands.usecss = { |
| 8385 action: function(value) { |
| 8386 // "If value is an ASCII case-insensitive match for the string "false", |
| 8387 // set the CSS styling flag to true. Otherwise, set the CSS styling |
| 8388 // flag to false. Either way, return true." |
| 8389 cssStylingFlag = String(value).toLowerCase() == "false"; |
| 8390 return true; |
| 8391 } |
| 8392 }; |
| 8393 //@} |
| 8394 |
| 8395 // Some final setup |
| 8396 //@{ |
| 8397 (function() { |
| 8398 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit |
| 8399 // temporary, which means I need an extra closure to not leak the temporaries |
| 8400 // into the global namespace. >:( |
| 8401 var commandNames = []; |
| 8402 for (var command in commands) { |
| 8403 commandNames.push(command); |
| 8404 } |
| 8405 commandNames.forEach(function(command) { |
| 8406 // "If a command does not have a relevant CSS property specified, it |
| 8407 // defaults to null." |
| 8408 if (!("relevantCssProperty" in commands[command])) { |
| 8409 commands[command].relevantCssProperty = null; |
| 8410 } |
| 8411 |
| 8412 // "If a command has inline command activated values defined but nothing |
| 8413 // else defines when it is indeterminate, it is indeterminate if among |
| 8414 // formattable nodes effectively contained in the active range, there is at |
| 8415 // least one whose effective command value is one of the given values and |
| 8416 // at least one whose effective command value is not one of the given |
| 8417 // values." |
| 8418 if ("inlineCommandActivatedValues" in commands[command] |
| 8419 && !("indeterm" in commands[command])) { |
| 8420 commands[command].indeterm = function() { |
| 8421 if (!getActiveRange()) { |
| 8422 return false; |
| 8423 } |
| 8424 |
| 8425 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFor
mattableNode) |
| 8426 .map(function(node) { return getEffectiveCommandValue(node, comm
and) }); |
| 8427 |
| 8428 var matchingValues = values.filter(function(value) { |
| 8429 return commands[command].inlineCommandActivatedValues.indexOf(va
lue) != -1; |
| 8430 }); |
| 8431 |
| 8432 return matchingValues.length >= 1 |
| 8433 && values.length - matchingValues.length >= 1; |
| 8434 }; |
| 8435 } |
| 8436 |
| 8437 // "If a command has inline command activated values defined, its state is |
| 8438 // true if either no formattable node is effectively contained in the |
| 8439 // active range, and the active range's start node's effective command |
| 8440 // value is one of the given values; or if there is at least one |
| 8441 // formattable node effectively contained in the active range, and all of |
| 8442 // them have an effective command value equal to one of the given values." |
| 8443 if ("inlineCommandActivatedValues" in commands[command]) { |
| 8444 commands[command].state = function() { |
| 8445 if (!getActiveRange()) { |
| 8446 return false; |
| 8447 } |
| 8448 |
| 8449 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isForm
attableNode); |
| 8450 |
| 8451 if (nodes.length == 0) { |
| 8452 return commands[command].inlineCommandActivatedValues |
| 8453 .indexOf(getEffectiveCommandValue(getActiveRange().startCont
ainer, command)) != -1; |
| 8454 } else { |
| 8455 return nodes.every(function(node) { |
| 8456 return commands[command].inlineCommandActivatedValues |
| 8457 .indexOf(getEffectiveCommandValue(node, command)) != -1; |
| 8458 }); |
| 8459 } |
| 8460 }; |
| 8461 } |
| 8462 |
| 8463 // "If a command is a standard inline value command, it is indeterminate if |
| 8464 // among formattable nodes that are effectively contained in the active |
| 8465 // range, there are two that have distinct effective command values. Its |
| 8466 // value is the effective command value of the first formattable node that |
| 8467 // is effectively contained in the active range; or if there is no such |
| 8468 // node, the effective command value of the active range's start node; or |
| 8469 // if that is null, the empty string." |
| 8470 if ("standardInlineValueCommand" in commands[command]) { |
| 8471 commands[command].indeterm = function() { |
| 8472 if (!getActiveRange()) { |
| 8473 return false; |
| 8474 } |
| 8475 |
| 8476 var values = getAllEffectivelyContainedNodes(getActiveRange()) |
| 8477 .filter(isFormattableNode) |
| 8478 .map(function(node) { return getEffectiveCommandValue(node, comm
and) }); |
| 8479 for (var i = 1; i < values.length; i++) { |
| 8480 if (values[i] != values[i - 1]) { |
| 8481 return true; |
| 8482 } |
| 8483 } |
| 8484 return false; |
| 8485 }; |
| 8486 |
| 8487 commands[command].value = function() { |
| 8488 if (!getActiveRange()) { |
| 8489 return ""; |
| 8490 } |
| 8491 |
| 8492 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFo
rmattableNode)[0]; |
| 8493 |
| 8494 if (typeof refNode == "undefined") { |
| 8495 refNode = getActiveRange().startContainer; |
| 8496 } |
| 8497 |
| 8498 var ret = getEffectiveCommandValue(refNode, command); |
| 8499 if (ret === null) { |
| 8500 return ""; |
| 8501 } |
| 8502 return ret; |
| 8503 }; |
| 8504 } |
| 8505 |
| 8506 // "If a command preserves overrides, then before taking its action, the |
| 8507 // user agent must record current overrides. After taking the action, if |
| 8508 // the active range is collapsed, it must restore states and values from |
| 8509 // the recorded list." |
| 8510 if ("preservesOverrides" in commands[command]) { |
| 8511 var oldAction = commands[command].action; |
| 8512 |
| 8513 commands[command].action = function(value) { |
| 8514 var overrides = recordCurrentOverrides(); |
| 8515 var ret = oldAction(value); |
| 8516 if (getActiveRange().collapsed) { |
| 8517 restoreStatesAndValues(overrides); |
| 8518 } |
| 8519 return ret; |
| 8520 }; |
| 8521 } |
| 8522 }); |
| 8523 })(); |
| 8524 //@} |
| 8525 |
| 8526 // vim: foldmarker=@{,@} foldmethod=marker |
OLD | NEW |