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