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