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