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