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