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