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