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