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