dom/imptests/editing/implementation.js

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 "use strict";
michael@0 2
michael@0 3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
michael@0 4
michael@0 5 var cssStylingFlag = false;
michael@0 6
michael@0 7 var defaultSingleLineContainerName = "p";
michael@0 8
michael@0 9 // This is bad :(
michael@0 10 var globalRange = null;
michael@0 11
michael@0 12 // Commands are stored in a dictionary where we call their actions and such
michael@0 13 var commands = {};
michael@0 14
michael@0 15 ///////////////////////////////////////////////////////////////////////////////
michael@0 16 ////////////////////////////// Utility functions //////////////////////////////
michael@0 17 ///////////////////////////////////////////////////////////////////////////////
michael@0 18 //@{
michael@0 19
michael@0 20 function nextNode(node) {
michael@0 21 if (node.hasChildNodes()) {
michael@0 22 return node.firstChild;
michael@0 23 }
michael@0 24 return nextNodeDescendants(node);
michael@0 25 }
michael@0 26
michael@0 27 function previousNode(node) {
michael@0 28 if (node.previousSibling) {
michael@0 29 node = node.previousSibling;
michael@0 30 while (node.hasChildNodes()) {
michael@0 31 node = node.lastChild;
michael@0 32 }
michael@0 33 return node;
michael@0 34 }
michael@0 35 if (node.parentNode
michael@0 36 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
michael@0 37 return node.parentNode;
michael@0 38 }
michael@0 39 return null;
michael@0 40 }
michael@0 41
michael@0 42 function nextNodeDescendants(node) {
michael@0 43 while (node && !node.nextSibling) {
michael@0 44 node = node.parentNode;
michael@0 45 }
michael@0 46 if (!node) {
michael@0 47 return null;
michael@0 48 }
michael@0 49 return node.nextSibling;
michael@0 50 }
michael@0 51
michael@0 52 /**
michael@0 53 * Returns true if ancestor is an ancestor of descendant, false otherwise.
michael@0 54 */
michael@0 55 function isAncestor(ancestor, descendant) {
michael@0 56 return ancestor
michael@0 57 && descendant
michael@0 58 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
michael@0 59 }
michael@0 60
michael@0 61 /**
michael@0 62 * Returns true if ancestor is an ancestor of or equal to descendant, false
michael@0 63 * otherwise.
michael@0 64 */
michael@0 65 function isAncestorContainer(ancestor, descendant) {
michael@0 66 return (ancestor || descendant)
michael@0 67 && (ancestor == descendant || isAncestor(ancestor, descendant));
michael@0 68 }
michael@0 69
michael@0 70 /**
michael@0 71 * Returns true if descendant is a descendant of ancestor, false otherwise.
michael@0 72 */
michael@0 73 function isDescendant(descendant, ancestor) {
michael@0 74 return ancestor
michael@0 75 && descendant
michael@0 76 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
michael@0 77 }
michael@0 78
michael@0 79 /**
michael@0 80 * Returns true if node1 is before node2 in tree order, false otherwise.
michael@0 81 */
michael@0 82 function isBefore(node1, node2) {
michael@0 83 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING);
michael@0 84 }
michael@0 85
michael@0 86 /**
michael@0 87 * Returns true if node1 is after node2 in tree order, false otherwise.
michael@0 88 */
michael@0 89 function isAfter(node1, node2) {
michael@0 90 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING);
michael@0 91 }
michael@0 92
michael@0 93 function getAncestors(node) {
michael@0 94 var ancestors = [];
michael@0 95 while (node.parentNode) {
michael@0 96 ancestors.unshift(node.parentNode);
michael@0 97 node = node.parentNode;
michael@0 98 }
michael@0 99 return ancestors;
michael@0 100 }
michael@0 101
michael@0 102 function getInclusiveAncestors(node) {
michael@0 103 return getAncestors(node).concat(node);
michael@0 104 }
michael@0 105
michael@0 106 function getDescendants(node) {
michael@0 107 var descendants = [];
michael@0 108 var stop = nextNodeDescendants(node);
michael@0 109 while ((node = nextNode(node))
michael@0 110 && node != stop) {
michael@0 111 descendants.push(node);
michael@0 112 }
michael@0 113 return descendants;
michael@0 114 }
michael@0 115
michael@0 116 function getInclusiveDescendants(node) {
michael@0 117 return [node].concat(getDescendants(node));
michael@0 118 }
michael@0 119
michael@0 120 function convertProperty(property) {
michael@0 121 // Special-case for now
michael@0 122 var map = {
michael@0 123 "fontFamily": "font-family",
michael@0 124 "fontSize": "font-size",
michael@0 125 "fontStyle": "font-style",
michael@0 126 "fontWeight": "font-weight",
michael@0 127 "textDecoration": "text-decoration",
michael@0 128 };
michael@0 129 if (typeof map[property] != "undefined") {
michael@0 130 return map[property];
michael@0 131 }
michael@0 132
michael@0 133 return property;
michael@0 134 }
michael@0 135
michael@0 136 // Return the <font size=X> value for the given CSS size, or undefined if there
michael@0 137 // is none.
michael@0 138 function cssSizeToLegacy(cssVal) {
michael@0 139 return {
michael@0 140 "x-small": 1,
michael@0 141 "small": 2,
michael@0 142 "medium": 3,
michael@0 143 "large": 4,
michael@0 144 "x-large": 5,
michael@0 145 "xx-large": 6,
michael@0 146 "xxx-large": 7
michael@0 147 }[cssVal];
michael@0 148 }
michael@0 149
michael@0 150 // Return the CSS size given a legacy size.
michael@0 151 function legacySizeToCss(legacyVal) {
michael@0 152 return {
michael@0 153 1: "x-small",
michael@0 154 2: "small",
michael@0 155 3: "medium",
michael@0 156 4: "large",
michael@0 157 5: "x-large",
michael@0 158 6: "xx-large",
michael@0 159 7: "xxx-large",
michael@0 160 }[legacyVal];
michael@0 161 }
michael@0 162
michael@0 163 // Opera 11 puts HTML elements in the null namespace, it seems.
michael@0 164 function isHtmlNamespace(ns) {
michael@0 165 return ns === null
michael@0 166 || ns === htmlNamespace;
michael@0 167 }
michael@0 168
michael@0 169 // "the directionality" from HTML. I don't bother caring about non-HTML
michael@0 170 // elements.
michael@0 171 //
michael@0 172 // "The directionality of an element is either 'ltr' or 'rtl', and is
michael@0 173 // determined as per the first appropriate set of steps from the following
michael@0 174 // list:"
michael@0 175 function getDirectionality(element) {
michael@0 176 // "If the element's dir attribute is in the ltr state
michael@0 177 // The directionality of the element is 'ltr'."
michael@0 178 if (element.dir == "ltr") {
michael@0 179 return "ltr";
michael@0 180 }
michael@0 181
michael@0 182 // "If the element's dir attribute is in the rtl state
michael@0 183 // The directionality of the element is 'rtl'."
michael@0 184 if (element.dir == "rtl") {
michael@0 185 return "rtl";
michael@0 186 }
michael@0 187
michael@0 188 // "If the element's dir attribute is in the auto state
michael@0 189 // "If the element is a bdi element and the dir attribute is not in a
michael@0 190 // defined state (i.e. it is not present or has an invalid value)
michael@0 191 // [lots of complicated stuff]
michael@0 192 //
michael@0 193 // Skip this, since no browser implements it anyway.
michael@0 194
michael@0 195 // "If the element is a root element and the dir attribute is not in a
michael@0 196 // defined state (i.e. it is not present or has an invalid value)
michael@0 197 // The directionality of the element is 'ltr'."
michael@0 198 if (!isHtmlElement(element.parentNode)) {
michael@0 199 return "ltr";
michael@0 200 }
michael@0 201
michael@0 202 // "If the element has a parent element and the dir attribute is not in a
michael@0 203 // defined state (i.e. it is not present or has an invalid value)
michael@0 204 // The directionality of the element is the same as the element's
michael@0 205 // parent element's directionality."
michael@0 206 return getDirectionality(element.parentNode);
michael@0 207 }
michael@0 208
michael@0 209 //@}
michael@0 210
michael@0 211 ///////////////////////////////////////////////////////////////////////////////
michael@0 212 ///////////////////////////// DOM Range functions /////////////////////////////
michael@0 213 ///////////////////////////////////////////////////////////////////////////////
michael@0 214 //@{
michael@0 215
michael@0 216 function getNodeIndex(node) {
michael@0 217 var ret = 0;
michael@0 218 while (node.previousSibling) {
michael@0 219 ret++;
michael@0 220 node = node.previousSibling;
michael@0 221 }
michael@0 222 return ret;
michael@0 223 }
michael@0 224
michael@0 225 // "The length of a Node node is the following, depending on node:
michael@0 226 //
michael@0 227 // ProcessingInstruction
michael@0 228 // DocumentType
michael@0 229 // Always 0.
michael@0 230 // Text
michael@0 231 // Comment
michael@0 232 // node's length.
michael@0 233 // Any other node
michael@0 234 // node's childNodes's length."
michael@0 235 function getNodeLength(node) {
michael@0 236 switch (node.nodeType) {
michael@0 237 case Node.PROCESSING_INSTRUCTION_NODE:
michael@0 238 case Node.DOCUMENT_TYPE_NODE:
michael@0 239 return 0;
michael@0 240
michael@0 241 case Node.TEXT_NODE:
michael@0 242 case Node.COMMENT_NODE:
michael@0 243 return node.length;
michael@0 244
michael@0 245 default:
michael@0 246 return node.childNodes.length;
michael@0 247 }
michael@0 248 }
michael@0 249
michael@0 250 /**
michael@0 251 * The position of two boundary points relative to one another, as defined by
michael@0 252 * DOM Range.
michael@0 253 */
michael@0 254 function getPosition(nodeA, offsetA, nodeB, offsetB) {
michael@0 255 // "If node A is the same as node B, return equal if offset A equals offset
michael@0 256 // B, before if offset A is less than offset B, and after if offset A is
michael@0 257 // greater than offset B."
michael@0 258 if (nodeA == nodeB) {
michael@0 259 if (offsetA == offsetB) {
michael@0 260 return "equal";
michael@0 261 }
michael@0 262 if (offsetA < offsetB) {
michael@0 263 return "before";
michael@0 264 }
michael@0 265 if (offsetA > offsetB) {
michael@0 266 return "after";
michael@0 267 }
michael@0 268 }
michael@0 269
michael@0 270 // "If node A is after node B in tree order, compute the position of (node
michael@0 271 // B, offset B) relative to (node A, offset A). If it is before, return
michael@0 272 // after. If it is after, return before."
michael@0 273 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
michael@0 274 var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
michael@0 275 if (pos == "before") {
michael@0 276 return "after";
michael@0 277 }
michael@0 278 if (pos == "after") {
michael@0 279 return "before";
michael@0 280 }
michael@0 281 }
michael@0 282
michael@0 283 // "If node A is an ancestor of node B:"
michael@0 284 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
michael@0 285 // "Let child equal node B."
michael@0 286 var child = nodeB;
michael@0 287
michael@0 288 // "While child is not a child of node A, set child to its parent."
michael@0 289 while (child.parentNode != nodeA) {
michael@0 290 child = child.parentNode;
michael@0 291 }
michael@0 292
michael@0 293 // "If the index of child is less than offset A, return after."
michael@0 294 if (getNodeIndex(child) < offsetA) {
michael@0 295 return "after";
michael@0 296 }
michael@0 297 }
michael@0 298
michael@0 299 // "Return before."
michael@0 300 return "before";
michael@0 301 }
michael@0 302
michael@0 303 /**
michael@0 304 * Returns the furthest ancestor of a Node as defined by DOM Range.
michael@0 305 */
michael@0 306 function getFurthestAncestor(node) {
michael@0 307 var root = node;
michael@0 308 while (root.parentNode != null) {
michael@0 309 root = root.parentNode;
michael@0 310 }
michael@0 311 return root;
michael@0 312 }
michael@0 313
michael@0 314 /**
michael@0 315 * "contained" as defined by DOM Range: "A Node node is contained in a range
michael@0 316 * range if node's furthest ancestor is the same as range's root, and (node, 0)
michael@0 317 * is after range's start, and (node, length of node) is before range's end."
michael@0 318 */
michael@0 319 function isContained(node, range) {
michael@0 320 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
michael@0 321 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
michael@0 322
michael@0 323 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer)
michael@0 324 && pos1 == "after"
michael@0 325 && pos2 == "before";
michael@0 326 }
michael@0 327
michael@0 328 /**
michael@0 329 * Return all nodes contained in range that the provided function returns true
michael@0 330 * for, omitting any with an ancestor already being returned.
michael@0 331 */
michael@0 332 function getContainedNodes(range, condition) {
michael@0 333 if (typeof condition == "undefined") {
michael@0 334 condition = function() { return true };
michael@0 335 }
michael@0 336 var node = range.startContainer;
michael@0 337 if (node.hasChildNodes()
michael@0 338 && range.startOffset < node.childNodes.length) {
michael@0 339 // A child is contained
michael@0 340 node = node.childNodes[range.startOffset];
michael@0 341 } else if (range.startOffset == getNodeLength(node)) {
michael@0 342 // No descendant can be contained
michael@0 343 node = nextNodeDescendants(node);
michael@0 344 } else {
michael@0 345 // No children; this node at least can't be contained
michael@0 346 node = nextNode(node);
michael@0 347 }
michael@0 348
michael@0 349 var stop = range.endContainer;
michael@0 350 if (stop.hasChildNodes()
michael@0 351 && range.endOffset < stop.childNodes.length) {
michael@0 352 // The node after the last contained node is a child
michael@0 353 stop = stop.childNodes[range.endOffset];
michael@0 354 } else {
michael@0 355 // This node and/or some of its children might be contained
michael@0 356 stop = nextNodeDescendants(stop);
michael@0 357 }
michael@0 358
michael@0 359 var nodeList = [];
michael@0 360 while (isBefore(node, stop)) {
michael@0 361 if (isContained(node, range)
michael@0 362 && condition(node)) {
michael@0 363 nodeList.push(node);
michael@0 364 node = nextNodeDescendants(node);
michael@0 365 continue;
michael@0 366 }
michael@0 367 node = nextNode(node);
michael@0 368 }
michael@0 369 return nodeList;
michael@0 370 }
michael@0 371
michael@0 372 /**
michael@0 373 * As above, but includes nodes with an ancestor that's already been returned.
michael@0 374 */
michael@0 375 function getAllContainedNodes(range, condition) {
michael@0 376 if (typeof condition == "undefined") {
michael@0 377 condition = function() { return true };
michael@0 378 }
michael@0 379 var node = range.startContainer;
michael@0 380 if (node.hasChildNodes()
michael@0 381 && range.startOffset < node.childNodes.length) {
michael@0 382 // A child is contained
michael@0 383 node = node.childNodes[range.startOffset];
michael@0 384 } else if (range.startOffset == getNodeLength(node)) {
michael@0 385 // No descendant can be contained
michael@0 386 node = nextNodeDescendants(node);
michael@0 387 } else {
michael@0 388 // No children; this node at least can't be contained
michael@0 389 node = nextNode(node);
michael@0 390 }
michael@0 391
michael@0 392 var stop = range.endContainer;
michael@0 393 if (stop.hasChildNodes()
michael@0 394 && range.endOffset < stop.childNodes.length) {
michael@0 395 // The node after the last contained node is a child
michael@0 396 stop = stop.childNodes[range.endOffset];
michael@0 397 } else {
michael@0 398 // This node and/or some of its children might be contained
michael@0 399 stop = nextNodeDescendants(stop);
michael@0 400 }
michael@0 401
michael@0 402 var nodeList = [];
michael@0 403 while (isBefore(node, stop)) {
michael@0 404 if (isContained(node, range)
michael@0 405 && condition(node)) {
michael@0 406 nodeList.push(node);
michael@0 407 }
michael@0 408 node = nextNode(node);
michael@0 409 }
michael@0 410 return nodeList;
michael@0 411 }
michael@0 412
michael@0 413 // Returns either null, or something of the form rgb(x, y, z), or something of
michael@0 414 // the form rgb(x, y, z, w) with w != 0.
michael@0 415 function normalizeColor(color) {
michael@0 416 if (color.toLowerCase() == "currentcolor") {
michael@0 417 return null;
michael@0 418 }
michael@0 419
michael@0 420 if (normalizeColor.resultCache === undefined) {
michael@0 421 normalizeColor.resultCache = {};
michael@0 422 }
michael@0 423
michael@0 424 if (normalizeColor.resultCache[color] !== undefined) {
michael@0 425 return normalizeColor.resultCache[color];
michael@0 426 }
michael@0 427
michael@0 428 var originalColor = color;
michael@0 429
michael@0 430 var outerSpan = document.createElement("span");
michael@0 431 document.body.appendChild(outerSpan);
michael@0 432 outerSpan.style.color = "black";
michael@0 433
michael@0 434 var innerSpan = document.createElement("span");
michael@0 435 outerSpan.appendChild(innerSpan);
michael@0 436 innerSpan.style.color = color;
michael@0 437 color = getComputedStyle(innerSpan).color;
michael@0 438
michael@0 439 if (color == "rgb(0, 0, 0)") {
michael@0 440 // Maybe it's really black, maybe it's invalid.
michael@0 441 outerSpan.color = "white";
michael@0 442 color = getComputedStyle(innerSpan).color;
michael@0 443 if (color != "rgb(0, 0, 0)") {
michael@0 444 return normalizeColor.resultCache[originalColor] = null;
michael@0 445 }
michael@0 446 }
michael@0 447
michael@0 448 document.body.removeChild(outerSpan);
michael@0 449
michael@0 450 // I rely on the fact that browsers generally provide consistent syntax for
michael@0 451 // getComputedStyle(), although it's not standardized. There are only
michael@0 452 // three exceptions I found:
michael@0 453 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
michael@0 454 // IE10PP2 seems to do this sometimes.
michael@0 455 return normalizeColor.resultCache[originalColor] =
michael@0 456 color.replace("rgba", "rgb").replace(", 1)", ")");
michael@0 457 }
michael@0 458 if (color == "transparent") {
michael@0 459 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
michael@0 460 // the specified value is "transparent".
michael@0 461 return normalizeColor.resultCache[originalColor] =
michael@0 462 "rgba(0, 0, 0, 0)";
michael@0 463 }
michael@0 464 // Chrome 15 dev adds way too many significant figures. This isn't a full
michael@0 465 // fix, it just fixes one case that comes up in tests.
michael@0 466 color = color.replace(/, 0.496094\)$/, ", 0.5)");
michael@0 467 return normalizeColor.resultCache[originalColor] = color;
michael@0 468 }
michael@0 469
michael@0 470 // Returns either null, or something of the form #xxxxxx.
michael@0 471 function parseSimpleColor(color) {
michael@0 472 color = normalizeColor(color);
michael@0 473 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
michael@0 474 if (matches) {
michael@0 475 return "#"
michael@0 476 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
michael@0 477 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
michael@0 478 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
michael@0 479 }
michael@0 480 return null;
michael@0 481 }
michael@0 482
michael@0 483 //@}
michael@0 484
michael@0 485 //////////////////////////////////////////////////////////////////////////////
michael@0 486 /////////////////////////// Edit command functions ///////////////////////////
michael@0 487 //////////////////////////////////////////////////////////////////////////////
michael@0 488
michael@0 489 /////////////////////////////////////////////////
michael@0 490 ///// Methods of the HTMLDocument interface /////
michael@0 491 /////////////////////////////////////////////////
michael@0 492 //@{
michael@0 493
michael@0 494 var executionStackDepth = 0;
michael@0 495
michael@0 496 // Helper function for common behavior.
michael@0 497 function editCommandMethod(command, range, callback) {
michael@0 498 // Set up our global range magic, but only if we're the outermost function
michael@0 499 if (executionStackDepth == 0 && typeof range != "undefined") {
michael@0 500 globalRange = range;
michael@0 501 } else if (executionStackDepth == 0) {
michael@0 502 globalRange = null;
michael@0 503 globalRange = getActiveRange();
michael@0 504 }
michael@0 505
michael@0 506 executionStackDepth++;
michael@0 507 try {
michael@0 508 var ret = callback();
michael@0 509 } catch(e) {
michael@0 510 executionStackDepth--;
michael@0 511 throw e;
michael@0 512 }
michael@0 513 executionStackDepth--;
michael@0 514 return ret;
michael@0 515 }
michael@0 516
michael@0 517 function myExecCommand(command, showUi, value, range) {
michael@0 518 // "All of these methods must treat their command argument ASCII
michael@0 519 // case-insensitively."
michael@0 520 command = command.toLowerCase();
michael@0 521
michael@0 522 // "If only one argument was provided, let show UI be false."
michael@0 523 //
michael@0 524 // If range was passed, I can't actually detect how many args were passed
michael@0 525 // . . .
michael@0 526 if (arguments.length == 1
michael@0 527 || (arguments.length >=4 && typeof showUi == "undefined")) {
michael@0 528 showUi = false;
michael@0 529 }
michael@0 530
michael@0 531 // "If only one or two arguments were provided, let value be the empty
michael@0 532 // string."
michael@0 533 if (arguments.length <= 2
michael@0 534 || (arguments.length >=4 && typeof value == "undefined")) {
michael@0 535 value = "";
michael@0 536 }
michael@0 537
michael@0 538 return editCommandMethod(command, range, (function(command, showUi, value) { return function() {
michael@0 539 // "If command is not supported or not enabled, return false."
michael@0 540 if (!(command in commands) || !myQueryCommandEnabled(command)) {
michael@0 541 return false;
michael@0 542 }
michael@0 543
michael@0 544 // "Take the action for command, passing value to the instructions as an
michael@0 545 // argument."
michael@0 546 var ret = commands[command].action(value);
michael@0 547
michael@0 548 // Check for bugs
michael@0 549 if (ret !== true && ret !== false) {
michael@0 550 throw "execCommand() didn't return true or false: " + ret;
michael@0 551 }
michael@0 552
michael@0 553 // "If the previous step returned false, return false."
michael@0 554 if (ret === false) {
michael@0 555 return false;
michael@0 556 }
michael@0 557
michael@0 558 // "Return true."
michael@0 559 return true;
michael@0 560 }})(command, showUi, value));
michael@0 561 }
michael@0 562
michael@0 563 function myQueryCommandEnabled(command, range) {
michael@0 564 // "All of these methods must treat their command argument ASCII
michael@0 565 // case-insensitively."
michael@0 566 command = command.toLowerCase();
michael@0 567
michael@0 568 return editCommandMethod(command, range, (function(command) { return function() {
michael@0 569 // "Return true if command is both supported and enabled, false
michael@0 570 // otherwise."
michael@0 571 if (!(command in commands)) {
michael@0 572 return false;
michael@0 573 }
michael@0 574
michael@0 575 // "Among commands defined in this specification, those listed in
michael@0 576 // Miscellaneous commands are always enabled, except for the cut
michael@0 577 // command and the paste command. The other commands defined here are
michael@0 578 // enabled if the active range is not null, its start node is either
michael@0 579 // editable or an editing host, its end node is either editable or an
michael@0 580 // editing host, and there is some editing host that is an inclusive
michael@0 581 // ancestor of both its start node and its end node."
michael@0 582 return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss",
michael@0 583 "usecss"].indexOf(command) != -1
michael@0 584 || (
michael@0 585 getActiveRange() !== null
michael@0 586 && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer))
michael@0 587 && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer))
michael@0 588 && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost))
michael@0 589 );
michael@0 590 }})(command));
michael@0 591 }
michael@0 592
michael@0 593 function myQueryCommandIndeterm(command, range) {
michael@0 594 // "All of these methods must treat their command argument ASCII
michael@0 595 // case-insensitively."
michael@0 596 command = command.toLowerCase();
michael@0 597
michael@0 598 return editCommandMethod(command, range, (function(command) { return function() {
michael@0 599 // "If command is not supported or has no indeterminacy, return false."
michael@0 600 if (!(command in commands) || !("indeterm" in commands[command])) {
michael@0 601 return false;
michael@0 602 }
michael@0 603
michael@0 604 // "Return true if command is indeterminate, otherwise false."
michael@0 605 return commands[command].indeterm();
michael@0 606 }})(command));
michael@0 607 }
michael@0 608
michael@0 609 function myQueryCommandState(command, range) {
michael@0 610 // "All of these methods must treat their command argument ASCII
michael@0 611 // case-insensitively."
michael@0 612 command = command.toLowerCase();
michael@0 613
michael@0 614 return editCommandMethod(command, range, (function(command) { return function() {
michael@0 615 // "If command is not supported or has no state, return false."
michael@0 616 if (!(command in commands) || !("state" in commands[command])) {
michael@0 617 return false;
michael@0 618 }
michael@0 619
michael@0 620 // "If the state override for command is set, return it."
michael@0 621 if (typeof getStateOverride(command) != "undefined") {
michael@0 622 return getStateOverride(command);
michael@0 623 }
michael@0 624
michael@0 625 // "Return true if command's state is true, otherwise false."
michael@0 626 return commands[command].state();
michael@0 627 }})(command));
michael@0 628 }
michael@0 629
michael@0 630 // "When the queryCommandSupported(command) method on the HTMLDocument
michael@0 631 // interface is invoked, the user agent must return true if command is
michael@0 632 // supported, and false otherwise."
michael@0 633 function myQueryCommandSupported(command) {
michael@0 634 // "All of these methods must treat their command argument ASCII
michael@0 635 // case-insensitively."
michael@0 636 command = command.toLowerCase();
michael@0 637
michael@0 638 return command in commands;
michael@0 639 }
michael@0 640
michael@0 641 function myQueryCommandValue(command, range) {
michael@0 642 // "All of these methods must treat their command argument ASCII
michael@0 643 // case-insensitively."
michael@0 644 command = command.toLowerCase();
michael@0 645
michael@0 646 return editCommandMethod(command, range, function() {
michael@0 647 // "If command is not supported or has no value, return the empty string."
michael@0 648 if (!(command in commands) || !("value" in commands[command])) {
michael@0 649 return "";
michael@0 650 }
michael@0 651
michael@0 652 // "If command is "fontSize" and its value override is set, convert the
michael@0 653 // value override to an integer number of pixels and return the legacy
michael@0 654 // font size for the result."
michael@0 655 if (command == "fontsize"
michael@0 656 && getValueOverride("fontsize") !== undefined) {
michael@0 657 return getLegacyFontSize(getValueOverride("fontsize"));
michael@0 658 }
michael@0 659
michael@0 660 // "If the value override for command is set, return it."
michael@0 661 if (typeof getValueOverride(command) != "undefined") {
michael@0 662 return getValueOverride(command);
michael@0 663 }
michael@0 664
michael@0 665 // "Return command's value."
michael@0 666 return commands[command].value();
michael@0 667 });
michael@0 668 }
michael@0 669 //@}
michael@0 670
michael@0 671 //////////////////////////////
michael@0 672 ///// Common definitions /////
michael@0 673 //////////////////////////////
michael@0 674 //@{
michael@0 675
michael@0 676 // "An HTML element is an Element whose namespace is the HTML namespace."
michael@0 677 //
michael@0 678 // I allow an extra argument to more easily check whether something is a
michael@0 679 // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays
michael@0 680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul.
michael@0 681 function isHtmlElement(node, tags) {
michael@0 682 if (typeof tags == "string") {
michael@0 683 tags = [tags];
michael@0 684 }
michael@0 685 if (typeof tags == "object") {
michael@0 686 tags = tags.map(function(tag) { return tag.toUpperCase() });
michael@0 687 }
michael@0 688 return node
michael@0 689 && node.nodeType == Node.ELEMENT_NODE
michael@0 690 && isHtmlNamespace(node.namespaceURI)
michael@0 691 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1);
michael@0 692 }
michael@0 693
michael@0 694 // "A prohibited paragraph child name is "address", "article", "aside",
michael@0 695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
michael@0 696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
michael@0 697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
michael@0 698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
michael@0 699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
michael@0 700 // "xmp"."
michael@0 701 var prohibitedParagraphChildNames = ["address", "article", "aside",
michael@0 702 "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
michael@0 703 "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
michael@0 704 "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
michael@0 705 "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
michael@0 706 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
michael@0 707 "xmp"];
michael@0 708
michael@0 709 // "A prohibited paragraph child is an HTML element whose local name is a
michael@0 710 // prohibited paragraph child name."
michael@0 711 function isProhibitedParagraphChild(node) {
michael@0 712 return isHtmlElement(node, prohibitedParagraphChildNames);
michael@0 713 }
michael@0 714
michael@0 715 // "A block node is either an Element whose "display" property does not have
michael@0 716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
michael@0 717 // Document, or a DocumentFragment."
michael@0 718 function isBlockNode(node) {
michael@0 719 return node
michael@0 720 && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1)
michael@0 721 || node.nodeType == Node.DOCUMENT_NODE
michael@0 722 || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE);
michael@0 723 }
michael@0 724
michael@0 725 // "An inline node is a node that is not a block node."
michael@0 726 function isInlineNode(node) {
michael@0 727 return node && !isBlockNode(node);
michael@0 728 }
michael@0 729
michael@0 730 // "An editing host is a node that is either an HTML element with a
michael@0 731 // contenteditable attribute set to the true state, or the HTML element child
michael@0 732 // of a Document whose designMode is enabled."
michael@0 733 function isEditingHost(node) {
michael@0 734 return node
michael@0 735 && isHtmlElement(node)
michael@0 736 && (node.contentEditable == "true"
michael@0 737 || (node.parentNode
michael@0 738 && node.parentNode.nodeType == Node.DOCUMENT_NODE
michael@0 739 && node.parentNode.designMode == "on"));
michael@0 740 }
michael@0 741
michael@0 742 // "Something is editable if it is a node; it is not an editing host; it does
michael@0 743 // not have a contenteditable attribute set to the false state; its parent is
michael@0 744 // an editing host or editable; and either it is an HTML element, or it is an
michael@0 745 // svg or math element, or it is not an Element and its parent is an HTML
michael@0 746 // element."
michael@0 747 function isEditable(node) {
michael@0 748 return node
michael@0 749 && !isEditingHost(node)
michael@0 750 && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false")
michael@0 751 && (isEditingHost(node.parentNode) || isEditable(node.parentNode))
michael@0 752 && (isHtmlElement(node)
michael@0 753 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg")
michael@0 754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math")
michael@0 755 || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)));
michael@0 756 }
michael@0 757
michael@0 758 // Helper function, not defined in the spec
michael@0 759 function hasEditableDescendants(node) {
michael@0 760 for (var i = 0; i < node.childNodes.length; i++) {
michael@0 761 if (isEditable(node.childNodes[i])
michael@0 762 || hasEditableDescendants(node.childNodes[i])) {
michael@0 763 return true;
michael@0 764 }
michael@0 765 }
michael@0 766 return false;
michael@0 767 }
michael@0 768
michael@0 769 // "The editing host of node is null if node is neither editable nor an editing
michael@0 770 // host; node itself, if node is an editing host; or the nearest ancestor of
michael@0 771 // node that is an editing host, if node is editable."
michael@0 772 function getEditingHostOf(node) {
michael@0 773 if (isEditingHost(node)) {
michael@0 774 return node;
michael@0 775 } else if (isEditable(node)) {
michael@0 776 var ancestor = node.parentNode;
michael@0 777 while (!isEditingHost(ancestor)) {
michael@0 778 ancestor = ancestor.parentNode;
michael@0 779 }
michael@0 780 return ancestor;
michael@0 781 } else {
michael@0 782 return null;
michael@0 783 }
michael@0 784 }
michael@0 785
michael@0 786 // "Two nodes are in the same editing host if the editing host of the first is
michael@0 787 // non-null and the same as the editing host of the second."
michael@0 788 function inSameEditingHost(node1, node2) {
michael@0 789 return getEditingHostOf(node1)
michael@0 790 && getEditingHostOf(node1) == getEditingHostOf(node2);
michael@0 791 }
michael@0 792
michael@0 793 // "A collapsed line break is a br that begins a line box which has nothing
michael@0 794 // else in it, and therefore has zero height."
michael@0 795 function isCollapsedLineBreak(br) {
michael@0 796 if (!isHtmlElement(br, "br")) {
michael@0 797 return false;
michael@0 798 }
michael@0 799
michael@0 800 // Add a zwsp after it and see if that changes the height of the nearest
michael@0 801 // non-inline parent. Note: this is not actually reliable, because the
michael@0 802 // parent might have a fixed height or something.
michael@0 803 var ref = br.parentNode;
michael@0 804 while (getComputedStyle(ref).display == "inline") {
michael@0 805 ref = ref.parentNode;
michael@0 806 }
michael@0 807 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
michael@0 808 ref.style.height = "auto";
michael@0 809 ref.style.maxHeight = "none";
michael@0 810 ref.style.minHeight = "0";
michael@0 811 var space = document.createTextNode("\u200b");
michael@0 812 var origHeight = ref.offsetHeight;
michael@0 813 if (origHeight == 0) {
michael@0 814 throw "isCollapsedLineBreak: original height is zero, bug?";
michael@0 815 }
michael@0 816 br.parentNode.insertBefore(space, br.nextSibling);
michael@0 817 var finalHeight = ref.offsetHeight;
michael@0 818 space.parentNode.removeChild(space);
michael@0 819 if (refStyle === null) {
michael@0 820 // Without the setAttribute() line, removeAttribute() doesn't work in
michael@0 821 // Chrome 14 dev. I have no idea why.
michael@0 822 ref.setAttribute("style", "");
michael@0 823 ref.removeAttribute("style");
michael@0 824 } else {
michael@0 825 ref.setAttribute("style", refStyle);
michael@0 826 }
michael@0 827
michael@0 828 // Allow some leeway in case the zwsp didn't create a whole new line, but
michael@0 829 // only made an existing line slightly higher. Firefox 6.0a2 shows this
michael@0 830 // behavior when the first line is bold.
michael@0 831 return origHeight < finalHeight - 5;
michael@0 832 }
michael@0 833
michael@0 834 // "An extraneous line break is a br that has no visual effect, in that
michael@0 835 // removing it from the DOM would not change layout, except that a br that is
michael@0 836 // the sole child of an li is not extraneous."
michael@0 837 //
michael@0 838 // FIXME: This doesn't work in IE, since IE ignores display: none in
michael@0 839 // contenteditable.
michael@0 840 function isExtraneousLineBreak(br) {
michael@0 841 if (!isHtmlElement(br, "br")) {
michael@0 842 return false;
michael@0 843 }
michael@0 844
michael@0 845 if (isHtmlElement(br.parentNode, "li")
michael@0 846 && br.parentNode.childNodes.length == 1) {
michael@0 847 return false;
michael@0 848 }
michael@0 849
michael@0 850 // Make the line break disappear and see if that changes the block's
michael@0 851 // height. Yes, this is an absurd hack. We have to reset height etc. on
michael@0 852 // the reference node because otherwise its height won't change if it's not
michael@0 853 // auto.
michael@0 854 var ref = br.parentNode;
michael@0 855 while (getComputedStyle(ref).display == "inline") {
michael@0 856 ref = ref.parentNode;
michael@0 857 }
michael@0 858 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
michael@0 859 ref.style.height = "auto";
michael@0 860 ref.style.maxHeight = "none";
michael@0 861 ref.style.minHeight = "0";
michael@0 862 var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null;
michael@0 863 var origHeight = ref.offsetHeight;
michael@0 864 if (origHeight == 0) {
michael@0 865 throw "isExtraneousLineBreak: original height is zero, bug?";
michael@0 866 }
michael@0 867 br.setAttribute("style", "display:none");
michael@0 868 var finalHeight = ref.offsetHeight;
michael@0 869 if (refStyle === null) {
michael@0 870 // Without the setAttribute() line, removeAttribute() doesn't work in
michael@0 871 // Chrome 14 dev. I have no idea why.
michael@0 872 ref.setAttribute("style", "");
michael@0 873 ref.removeAttribute("style");
michael@0 874 } else {
michael@0 875 ref.setAttribute("style", refStyle);
michael@0 876 }
michael@0 877 if (brStyle === null) {
michael@0 878 br.removeAttribute("style");
michael@0 879 } else {
michael@0 880 br.setAttribute("style", brStyle);
michael@0 881 }
michael@0 882
michael@0 883 return origHeight == finalHeight;
michael@0 884 }
michael@0 885
michael@0 886 // "A whitespace node is either a Text node whose data is the empty string; or
michael@0 887 // a Text node whose data consists only of one or more tabs (0x0009), line
michael@0 888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
michael@0 889 // parent is an Element whose resolved value for "white-space" is "normal" or
michael@0 890 // "nowrap"; or a Text node whose data consists only of one or more tabs
michael@0 891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
michael@0 892 // parent is an Element whose resolved value for "white-space" is "pre-line"."
michael@0 893 function isWhitespaceNode(node) {
michael@0 894 return node
michael@0 895 && node.nodeType == Node.TEXT_NODE
michael@0 896 && (node.data == ""
michael@0 897 || (
michael@0 898 /^[\t\n\r ]+$/.test(node.data)
michael@0 899 && node.parentNode
michael@0 900 && node.parentNode.nodeType == Node.ELEMENT_NODE
michael@0 901 && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1
michael@0 902 ) || (
michael@0 903 /^[\t\r ]+$/.test(node.data)
michael@0 904 && node.parentNode
michael@0 905 && node.parentNode.nodeType == Node.ELEMENT_NODE
michael@0 906 && getComputedStyle(node.parentNode).whiteSpace == "pre-line"
michael@0 907 ));
michael@0 908 }
michael@0 909
michael@0 910 // "node is a collapsed whitespace node if the following algorithm returns
michael@0 911 // true:"
michael@0 912 function isCollapsedWhitespaceNode(node) {
michael@0 913 // "If node is not a whitespace node, return false."
michael@0 914 if (!isWhitespaceNode(node)) {
michael@0 915 return false;
michael@0 916 }
michael@0 917
michael@0 918 // "If node's data is the empty string, return true."
michael@0 919 if (node.data == "") {
michael@0 920 return true;
michael@0 921 }
michael@0 922
michael@0 923 // "Let ancestor be node's parent."
michael@0 924 var ancestor = node.parentNode;
michael@0 925
michael@0 926 // "If ancestor is null, return true."
michael@0 927 if (!ancestor) {
michael@0 928 return true;
michael@0 929 }
michael@0 930
michael@0 931 // "If the "display" property of some ancestor of node has resolved value
michael@0 932 // "none", return true."
michael@0 933 if (getAncestors(node).some(function(ancestor) {
michael@0 934 return ancestor.nodeType == Node.ELEMENT_NODE
michael@0 935 && getComputedStyle(ancestor).display == "none";
michael@0 936 })) {
michael@0 937 return true;
michael@0 938 }
michael@0 939
michael@0 940 // "While ancestor is not a block node and its parent is not null, set
michael@0 941 // ancestor to its parent."
michael@0 942 while (!isBlockNode(ancestor)
michael@0 943 && ancestor.parentNode) {
michael@0 944 ancestor = ancestor.parentNode;
michael@0 945 }
michael@0 946
michael@0 947 // "Let reference be node."
michael@0 948 var reference = node;
michael@0 949
michael@0 950 // "While reference is a descendant of ancestor:"
michael@0 951 while (reference != ancestor) {
michael@0 952 // "Let reference be the node before it in tree order."
michael@0 953 reference = previousNode(reference);
michael@0 954
michael@0 955 // "If reference is a block node or a br, return true."
michael@0 956 if (isBlockNode(reference)
michael@0 957 || isHtmlElement(reference, "br")) {
michael@0 958 return true;
michael@0 959 }
michael@0 960
michael@0 961 // "If reference is a Text node that is not a whitespace node, or is an
michael@0 962 // img, break from this loop."
michael@0 963 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
michael@0 964 || isHtmlElement(reference, "img")) {
michael@0 965 break;
michael@0 966 }
michael@0 967 }
michael@0 968
michael@0 969 // "Let reference be node."
michael@0 970 reference = node;
michael@0 971
michael@0 972 // "While reference is a descendant of ancestor:"
michael@0 973 var stop = nextNodeDescendants(ancestor);
michael@0 974 while (reference != stop) {
michael@0 975 // "Let reference be the node after it in tree order, or null if there
michael@0 976 // is no such node."
michael@0 977 reference = nextNode(reference);
michael@0 978
michael@0 979 // "If reference is a block node or a br, return true."
michael@0 980 if (isBlockNode(reference)
michael@0 981 || isHtmlElement(reference, "br")) {
michael@0 982 return true;
michael@0 983 }
michael@0 984
michael@0 985 // "If reference is a Text node that is not a whitespace node, or is an
michael@0 986 // img, break from this loop."
michael@0 987 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
michael@0 988 || isHtmlElement(reference, "img")) {
michael@0 989 break;
michael@0 990 }
michael@0 991 }
michael@0 992
michael@0 993 // "Return false."
michael@0 994 return false;
michael@0 995 }
michael@0 996
michael@0 997 // "Something is visible if it is a node that either is a block node, or a Text
michael@0 998 // node that is not a collapsed whitespace node, or an img, or a br that is not
michael@0 999 // an extraneous line break, or any node with a visible descendant; excluding
michael@0 1000 // any node with an ancestor container Element whose "display" property has
michael@0 1001 // resolved value "none"."
michael@0 1002 function isVisible(node) {
michael@0 1003 if (!node) {
michael@0 1004 return false;
michael@0 1005 }
michael@0 1006
michael@0 1007 if (getAncestors(node).concat(node)
michael@0 1008 .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE })
michael@0 1009 .some(function(node) { return getComputedStyle(node).display == "none" })) {
michael@0 1010 return false;
michael@0 1011 }
michael@0 1012
michael@0 1013 if (isBlockNode(node)
michael@0 1014 || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
michael@0 1015 || isHtmlElement(node, "img")
michael@0 1016 || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) {
michael@0 1017 return true;
michael@0 1018 }
michael@0 1019
michael@0 1020 for (var i = 0; i < node.childNodes.length; i++) {
michael@0 1021 if (isVisible(node.childNodes[i])) {
michael@0 1022 return true;
michael@0 1023 }
michael@0 1024 }
michael@0 1025
michael@0 1026 return false;
michael@0 1027 }
michael@0 1028
michael@0 1029 // "Something is invisible if it is a node that is not visible."
michael@0 1030 function isInvisible(node) {
michael@0 1031 return node && !isVisible(node);
michael@0 1032 }
michael@0 1033
michael@0 1034 // "A collapsed block prop is either a collapsed line break that is not an
michael@0 1035 // extraneous line break, or an Element that is an inline node and whose
michael@0 1036 // children are all either invisible or collapsed block props and that has at
michael@0 1037 // least one child that is a collapsed block prop."
michael@0 1038 function isCollapsedBlockProp(node) {
michael@0 1039 if (isCollapsedLineBreak(node)
michael@0 1040 && !isExtraneousLineBreak(node)) {
michael@0 1041 return true;
michael@0 1042 }
michael@0 1043
michael@0 1044 if (!isInlineNode(node)
michael@0 1045 || node.nodeType != Node.ELEMENT_NODE) {
michael@0 1046 return false;
michael@0 1047 }
michael@0 1048
michael@0 1049 var hasCollapsedBlockPropChild = false;
michael@0 1050 for (var i = 0; i < node.childNodes.length; i++) {
michael@0 1051 if (!isInvisible(node.childNodes[i])
michael@0 1052 && !isCollapsedBlockProp(node.childNodes[i])) {
michael@0 1053 return false;
michael@0 1054 }
michael@0 1055 if (isCollapsedBlockProp(node.childNodes[i])) {
michael@0 1056 hasCollapsedBlockPropChild = true;
michael@0 1057 }
michael@0 1058 }
michael@0 1059
michael@0 1060 return hasCollapsedBlockPropChild;
michael@0 1061 }
michael@0 1062
michael@0 1063 // "The active range is the range of the selection given by calling
michael@0 1064 // getSelection() on the context object. (Thus the active range may be null.)"
michael@0 1065 //
michael@0 1066 // We cheat and return globalRange if that's defined. We also ensure that the
michael@0 1067 // active range meets the requirements that selection boundary points are
michael@0 1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
michael@0 1069 // descend from a Document.
michael@0 1070 function getActiveRange() {
michael@0 1071 var ret;
michael@0 1072 if (globalRange) {
michael@0 1073 ret = globalRange;
michael@0 1074 } else if (getSelection().rangeCount) {
michael@0 1075 ret = getSelection().getRangeAt(0);
michael@0 1076 } else {
michael@0 1077 return null;
michael@0 1078 }
michael@0 1079 if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1
michael@0 1080 || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1
michael@0 1081 || !ret.startContainer.ownerDocument
michael@0 1082 || !ret.endContainer.ownerDocument
michael@0 1083 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
michael@0 1084 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
michael@0 1085 throw "Invalid active range; test bug?";
michael@0 1086 }
michael@0 1087 return ret;
michael@0 1088 }
michael@0 1089
michael@0 1090 // "For some commands, each HTMLDocument must have a boolean state override
michael@0 1091 // and/or a string value override. These do not change the command's state or
michael@0 1092 // value, but change the way some algorithms behave, as specified in those
michael@0 1093 // algorithms' definitions. Initially, both must be unset for every command.
michael@0 1094 // Whenever the number of ranges in the Selection changes to something
michael@0 1095 // different, and whenever a boundary point of the range at a given index in
michael@0 1096 // the Selection changes to something different, the state override and value
michael@0 1097 // override must be unset for every command."
michael@0 1098 //
michael@0 1099 // We implement this crudely by using setters and getters. To verify that the
michael@0 1100 // selection hasn't changed, we copy the active range and just check the
michael@0 1101 // endpoints match. This isn't really correct, but it's good enough for us.
michael@0 1102 // Unset state/value overrides are undefined. We put everything in a function
michael@0 1103 // so no one can access anything except via the provided functions, since
michael@0 1104 // otherwise callers might mistakenly use outdated overrides (if the selection
michael@0 1105 // has changed).
michael@0 1106 var getStateOverride, setStateOverride, unsetStateOverride,
michael@0 1107 getValueOverride, setValueOverride, unsetValueOverride;
michael@0 1108 (function() {
michael@0 1109 var stateOverrides = {};
michael@0 1110 var valueOverrides = {};
michael@0 1111 var storedRange = null;
michael@0 1112
michael@0 1113 function resetOverrides() {
michael@0 1114 if (!storedRange
michael@0 1115 || storedRange.startContainer != getActiveRange().startContainer
michael@0 1116 || storedRange.endContainer != getActiveRange().endContainer
michael@0 1117 || storedRange.startOffset != getActiveRange().startOffset
michael@0 1118 || storedRange.endOffset != getActiveRange().endOffset) {
michael@0 1119 stateOverrides = {};
michael@0 1120 valueOverrides = {};
michael@0 1121 storedRange = getActiveRange().cloneRange();
michael@0 1122 }
michael@0 1123 }
michael@0 1124
michael@0 1125 getStateOverride = function(command) {
michael@0 1126 resetOverrides();
michael@0 1127 return stateOverrides[command];
michael@0 1128 };
michael@0 1129
michael@0 1130 setStateOverride = function(command, newState) {
michael@0 1131 resetOverrides();
michael@0 1132 stateOverrides[command] = newState;
michael@0 1133 };
michael@0 1134
michael@0 1135 unsetStateOverride = function(command) {
michael@0 1136 resetOverrides();
michael@0 1137 delete stateOverrides[command];
michael@0 1138 }
michael@0 1139
michael@0 1140 getValueOverride = function(command) {
michael@0 1141 resetOverrides();
michael@0 1142 return valueOverrides[command];
michael@0 1143 }
michael@0 1144
michael@0 1145 // "The value override for the backColor command must be the same as the
michael@0 1146 // value override for the hiliteColor command, such that setting one sets
michael@0 1147 // the other to the same thing and unsetting one unsets the other."
michael@0 1148 setValueOverride = function(command, newValue) {
michael@0 1149 resetOverrides();
michael@0 1150 valueOverrides[command] = newValue;
michael@0 1151 if (command == "backcolor") {
michael@0 1152 valueOverrides.hilitecolor = newValue;
michael@0 1153 } else if (command == "hilitecolor") {
michael@0 1154 valueOverrides.backcolor = newValue;
michael@0 1155 }
michael@0 1156 }
michael@0 1157
michael@0 1158 unsetValueOverride = function(command) {
michael@0 1159 resetOverrides();
michael@0 1160 delete valueOverrides[command];
michael@0 1161 if (command == "backcolor") {
michael@0 1162 delete valueOverrides.hilitecolor;
michael@0 1163 } else if (command == "hilitecolor") {
michael@0 1164 delete valueOverrides.backcolor;
michael@0 1165 }
michael@0 1166 }
michael@0 1167 })();
michael@0 1168
michael@0 1169 //@}
michael@0 1170
michael@0 1171 /////////////////////////////
michael@0 1172 ///// Common algorithms /////
michael@0 1173 /////////////////////////////
michael@0 1174
michael@0 1175 ///// Assorted common algorithms /////
michael@0 1176 //@{
michael@0 1177
michael@0 1178 // Magic array of extra ranges whose endpoints we want to preserve.
michael@0 1179 var extraRanges = [];
michael@0 1180
michael@0 1181 function movePreservingRanges(node, newParent, newIndex) {
michael@0 1182 // For convenience, I allow newIndex to be -1 to mean "insert at the end".
michael@0 1183 if (newIndex == -1) {
michael@0 1184 newIndex = newParent.childNodes.length;
michael@0 1185 }
michael@0 1186
michael@0 1187 // "When the user agent is to move a Node to a new location, preserving
michael@0 1188 // ranges, it must remove the Node from its original parent (if any), then
michael@0 1189 // insert it in the new location. In doing so, however, it must ignore the
michael@0 1190 // regular range mutation rules, and instead follow these rules:"
michael@0 1191
michael@0 1192 // "Let node be the moved Node, old parent and old index be the old parent
michael@0 1193 // (which may be null) and index, and new parent and new index be the new
michael@0 1194 // parent and index."
michael@0 1195 var oldParent = node.parentNode;
michael@0 1196 var oldIndex = getNodeIndex(node);
michael@0 1197
michael@0 1198 // We preserve the global range object, the ranges in the selection, and
michael@0 1199 // any range that's in the extraRanges array. Any other ranges won't get
michael@0 1200 // updated, because we have no references to them.
michael@0 1201 var ranges = [globalRange].concat(extraRanges);
michael@0 1202 for (var i = 0; i < getSelection().rangeCount; i++) {
michael@0 1203 ranges.push(getSelection().getRangeAt(i));
michael@0 1204 }
michael@0 1205 var boundaryPoints = [];
michael@0 1206 ranges.forEach(function(range) {
michael@0 1207 boundaryPoints.push([range.startContainer, range.startOffset]);
michael@0 1208 boundaryPoints.push([range.endContainer, range.endOffset]);
michael@0 1209 });
michael@0 1210
michael@0 1211 boundaryPoints.forEach(function(boundaryPoint) {
michael@0 1212 // "If a boundary point's node is the same as or a descendant of node,
michael@0 1213 // leave it unchanged, so it moves to the new location."
michael@0 1214 //
michael@0 1215 // No modifications necessary.
michael@0 1216
michael@0 1217 // "If a boundary point's node is new parent and its offset is greater
michael@0 1218 // than new index, add one to its offset."
michael@0 1219 if (boundaryPoint[0] == newParent
michael@0 1220 && boundaryPoint[1] > newIndex) {
michael@0 1221 boundaryPoint[1]++;
michael@0 1222 }
michael@0 1223
michael@0 1224 // "If a boundary point's node is old parent and its offset is old index or
michael@0 1225 // old index + 1, set its node to new parent and add new index − old index
michael@0 1226 // to its offset."
michael@0 1227 if (boundaryPoint[0] == oldParent
michael@0 1228 && (boundaryPoint[1] == oldIndex
michael@0 1229 || boundaryPoint[1] == oldIndex + 1)) {
michael@0 1230 boundaryPoint[0] = newParent;
michael@0 1231 boundaryPoint[1] += newIndex - oldIndex;
michael@0 1232 }
michael@0 1233
michael@0 1234 // "If a boundary point's node is old parent and its offset is greater than
michael@0 1235 // old index + 1, subtract one from its offset."
michael@0 1236 if (boundaryPoint[0] == oldParent
michael@0 1237 && boundaryPoint[1] > oldIndex + 1) {
michael@0 1238 boundaryPoint[1]--;
michael@0 1239 }
michael@0 1240 });
michael@0 1241
michael@0 1242 // Now actually move it and preserve the ranges.
michael@0 1243 if (newParent.childNodes.length == newIndex) {
michael@0 1244 newParent.appendChild(node);
michael@0 1245 } else {
michael@0 1246 newParent.insertBefore(node, newParent.childNodes[newIndex]);
michael@0 1247 }
michael@0 1248
michael@0 1249 globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
michael@0 1250 globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
michael@0 1251
michael@0 1252 for (var i = 0; i < extraRanges.length; i++) {
michael@0 1253 extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]);
michael@0 1254 extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]);
michael@0 1255 }
michael@0 1256
michael@0 1257 getSelection().removeAllRanges();
michael@0 1258 for (var i = 1 + extraRanges.length; i < ranges.length; i++) {
michael@0 1259 var newRange = document.createRange();
michael@0 1260 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
michael@0 1261 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
michael@0 1262 getSelection().addRange(newRange);
michael@0 1263 }
michael@0 1264 }
michael@0 1265
michael@0 1266 function setTagName(element, newName) {
michael@0 1267 // "If element is an HTML element with local name equal to new name, return
michael@0 1268 // element."
michael@0 1269 if (isHtmlElement(element, newName.toUpperCase())) {
michael@0 1270 return element;
michael@0 1271 }
michael@0 1272
michael@0 1273 // "If element's parent is null, return element."
michael@0 1274 if (!element.parentNode) {
michael@0 1275 return element;
michael@0 1276 }
michael@0 1277
michael@0 1278 // "Let replacement element be the result of calling createElement(new
michael@0 1279 // name) on the ownerDocument of element."
michael@0 1280 var replacementElement = element.ownerDocument.createElement(newName);
michael@0 1281
michael@0 1282 // "Insert replacement element into element's parent immediately before
michael@0 1283 // element."
michael@0 1284 element.parentNode.insertBefore(replacementElement, element);
michael@0 1285
michael@0 1286 // "Copy all attributes of element to replacement element, in order."
michael@0 1287 for (var i = 0; i < element.attributes.length; i++) {
michael@0 1288 replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value);
michael@0 1289 }
michael@0 1290
michael@0 1291 // "While element has children, append the first child of element as the
michael@0 1292 // last child of replacement element, preserving ranges."
michael@0 1293 while (element.childNodes.length) {
michael@0 1294 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length);
michael@0 1295 }
michael@0 1296
michael@0 1297 // "Remove element from its parent."
michael@0 1298 element.parentNode.removeChild(element);
michael@0 1299
michael@0 1300 // "Return replacement element."
michael@0 1301 return replacementElement;
michael@0 1302 }
michael@0 1303
michael@0 1304 function removeExtraneousLineBreaksBefore(node) {
michael@0 1305 // "Let ref be the previousSibling of node."
michael@0 1306 var ref = node.previousSibling;
michael@0 1307
michael@0 1308 // "If ref is null, abort these steps."
michael@0 1309 if (!ref) {
michael@0 1310 return;
michael@0 1311 }
michael@0 1312
michael@0 1313 // "While ref has children, set ref to its lastChild."
michael@0 1314 while (ref.hasChildNodes()) {
michael@0 1315 ref = ref.lastChild;
michael@0 1316 }
michael@0 1317
michael@0 1318 // "While ref is invisible but not an extraneous line break, and ref does
michael@0 1319 // not equal node's parent, set ref to the node before it in tree order."
michael@0 1320 while (isInvisible(ref)
michael@0 1321 && !isExtraneousLineBreak(ref)
michael@0 1322 && ref != node.parentNode) {
michael@0 1323 ref = previousNode(ref);
michael@0 1324 }
michael@0 1325
michael@0 1326 // "If ref is an editable extraneous line break, remove it from its
michael@0 1327 // parent."
michael@0 1328 if (isEditable(ref)
michael@0 1329 && isExtraneousLineBreak(ref)) {
michael@0 1330 ref.parentNode.removeChild(ref);
michael@0 1331 }
michael@0 1332 }
michael@0 1333
michael@0 1334 function removeExtraneousLineBreaksAtTheEndOf(node) {
michael@0 1335 // "Let ref be node."
michael@0 1336 var ref = node;
michael@0 1337
michael@0 1338 // "While ref has children, set ref to its lastChild."
michael@0 1339 while (ref.hasChildNodes()) {
michael@0 1340 ref = ref.lastChild;
michael@0 1341 }
michael@0 1342
michael@0 1343 // "While ref is invisible but not an extraneous line break, and ref does
michael@0 1344 // not equal node, set ref to the node before it in tree order."
michael@0 1345 while (isInvisible(ref)
michael@0 1346 && !isExtraneousLineBreak(ref)
michael@0 1347 && ref != node) {
michael@0 1348 ref = previousNode(ref);
michael@0 1349 }
michael@0 1350
michael@0 1351 // "If ref is an editable extraneous line break:"
michael@0 1352 if (isEditable(ref)
michael@0 1353 && isExtraneousLineBreak(ref)) {
michael@0 1354 // "While ref's parent is editable and invisible, set ref to its
michael@0 1355 // parent."
michael@0 1356 while (isEditable(ref.parentNode)
michael@0 1357 && isInvisible(ref.parentNode)) {
michael@0 1358 ref = ref.parentNode;
michael@0 1359 }
michael@0 1360
michael@0 1361 // "Remove ref from its parent."
michael@0 1362 ref.parentNode.removeChild(ref);
michael@0 1363 }
michael@0 1364 }
michael@0 1365
michael@0 1366 // "To remove extraneous line breaks from a node, first remove extraneous line
michael@0 1367 // breaks before it, then remove extraneous line breaks at the end of it."
michael@0 1368 function removeExtraneousLineBreaksFrom(node) {
michael@0 1369 removeExtraneousLineBreaksBefore(node);
michael@0 1370 removeExtraneousLineBreaksAtTheEndOf(node);
michael@0 1371 }
michael@0 1372
michael@0 1373 //@}
michael@0 1374 ///// Wrapping a list of nodes /////
michael@0 1375 //@{
michael@0 1376
michael@0 1377 function wrap(nodeList, siblingCriteria, newParentInstructions) {
michael@0 1378 // "If not provided, sibling criteria returns false and new parent
michael@0 1379 // instructions returns null."
michael@0 1380 if (typeof siblingCriteria == "undefined") {
michael@0 1381 siblingCriteria = function() { return false };
michael@0 1382 }
michael@0 1383 if (typeof newParentInstructions == "undefined") {
michael@0 1384 newParentInstructions = function() { return null };
michael@0 1385 }
michael@0 1386
michael@0 1387 // "If every member of node list is invisible, and none is a br, return
michael@0 1388 // null and abort these steps."
michael@0 1389 if (nodeList.every(isInvisible)
michael@0 1390 && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) {
michael@0 1391 return null;
michael@0 1392 }
michael@0 1393
michael@0 1394 // "If node list's first member's parent is null, return null and abort
michael@0 1395 // these steps."
michael@0 1396 if (!nodeList[0].parentNode) {
michael@0 1397 return null;
michael@0 1398 }
michael@0 1399
michael@0 1400 // "If node list's last member is an inline node that's not a br, and node
michael@0 1401 // list's last member's nextSibling is a br, append that br to node list."
michael@0 1402 if (isInlineNode(nodeList[nodeList.length - 1])
michael@0 1403 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
michael@0 1404 && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
michael@0 1405 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
michael@0 1406 }
michael@0 1407
michael@0 1408 // "While node list's first member's previousSibling is invisible, prepend
michael@0 1409 // it to node list."
michael@0 1410 while (isInvisible(nodeList[0].previousSibling)) {
michael@0 1411 nodeList.unshift(nodeList[0].previousSibling);
michael@0 1412 }
michael@0 1413
michael@0 1414 // "While node list's last member's nextSibling is invisible, append it to
michael@0 1415 // node list."
michael@0 1416 while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) {
michael@0 1417 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
michael@0 1418 }
michael@0 1419
michael@0 1420 // "If the previousSibling of the first member of node list is editable and
michael@0 1421 // running sibling criteria on it returns true, let new parent be the
michael@0 1422 // previousSibling of the first member of node list."
michael@0 1423 var newParent;
michael@0 1424 if (isEditable(nodeList[0].previousSibling)
michael@0 1425 && siblingCriteria(nodeList[0].previousSibling)) {
michael@0 1426 newParent = nodeList[0].previousSibling;
michael@0 1427
michael@0 1428 // "Otherwise, if the nextSibling of the last member of node list is
michael@0 1429 // editable and running sibling criteria on it returns true, let new parent
michael@0 1430 // be the nextSibling of the last member of node list."
michael@0 1431 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
michael@0 1432 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
michael@0 1433 newParent = nodeList[nodeList.length - 1].nextSibling;
michael@0 1434
michael@0 1435 // "Otherwise, run new parent instructions, and let new parent be the
michael@0 1436 // result."
michael@0 1437 } else {
michael@0 1438 newParent = newParentInstructions();
michael@0 1439 }
michael@0 1440
michael@0 1441 // "If new parent is null, abort these steps and return null."
michael@0 1442 if (!newParent) {
michael@0 1443 return null;
michael@0 1444 }
michael@0 1445
michael@0 1446 // "If new parent's parent is null:"
michael@0 1447 if (!newParent.parentNode) {
michael@0 1448 // "Insert new parent into the parent of the first member of node list
michael@0 1449 // immediately before the first member of node list."
michael@0 1450 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
michael@0 1451
michael@0 1452 // "If any range has a boundary point with node equal to the parent of
michael@0 1453 // new parent and offset equal to the index of new parent, add one to
michael@0 1454 // that boundary point's offset."
michael@0 1455 //
michael@0 1456 // Only try to fix the global range.
michael@0 1457 if (globalRange.startContainer == newParent.parentNode
michael@0 1458 && globalRange.startOffset == getNodeIndex(newParent)) {
michael@0 1459 globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1);
michael@0 1460 }
michael@0 1461 if (globalRange.endContainer == newParent.parentNode
michael@0 1462 && globalRange.endOffset == getNodeIndex(newParent)) {
michael@0 1463 globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1);
michael@0 1464 }
michael@0 1465 }
michael@0 1466
michael@0 1467 // "Let original parent be the parent of the first member of node list."
michael@0 1468 var originalParent = nodeList[0].parentNode;
michael@0 1469
michael@0 1470 // "If new parent is before the first member of node list in tree order:"
michael@0 1471 if (isBefore(newParent, nodeList[0])) {
michael@0 1472 // "If new parent is not an inline node, but the last visible child of
michael@0 1473 // new parent and the first visible member of node list are both inline
michael@0 1474 // nodes, and the last child of new parent is not a br, call
michael@0 1475 // createElement("br") on the ownerDocument of new parent and append
michael@0 1476 // the result as the last child of new parent."
michael@0 1477 if (!isInlineNode(newParent)
michael@0 1478 && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0])
michael@0 1479 && isInlineNode(nodeList.filter(isVisible)[0])
michael@0 1480 && !isHtmlElement(newParent.lastChild, "BR")) {
michael@0 1481 newParent.appendChild(newParent.ownerDocument.createElement("br"));
michael@0 1482 }
michael@0 1483
michael@0 1484 // "For each node in node list, append node as the last child of new
michael@0 1485 // parent, preserving ranges."
michael@0 1486 for (var i = 0; i < nodeList.length; i++) {
michael@0 1487 movePreservingRanges(nodeList[i], newParent, -1);
michael@0 1488 }
michael@0 1489
michael@0 1490 // "Otherwise:"
michael@0 1491 } else {
michael@0 1492 // "If new parent is not an inline node, but the first visible child of
michael@0 1493 // new parent and the last visible member of node list are both inline
michael@0 1494 // nodes, and the last member of node list is not a br, call
michael@0 1495 // createElement("br") on the ownerDocument of new parent and insert
michael@0 1496 // the result as the first child of new parent."
michael@0 1497 if (!isInlineNode(newParent)
michael@0 1498 && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0])
michael@0 1499 && isInlineNode(nodeList.filter(isVisible).slice(-1)[0])
michael@0 1500 && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) {
michael@0 1501 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
michael@0 1502 }
michael@0 1503
michael@0 1504 // "For each node in node list, in reverse order, insert node as the
michael@0 1505 // first child of new parent, preserving ranges."
michael@0 1506 for (var i = nodeList.length - 1; i >= 0; i--) {
michael@0 1507 movePreservingRanges(nodeList[i], newParent, 0);
michael@0 1508 }
michael@0 1509 }
michael@0 1510
michael@0 1511 // "If original parent is editable and has no children, remove it from its
michael@0 1512 // parent."
michael@0 1513 if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
michael@0 1514 originalParent.parentNode.removeChild(originalParent);
michael@0 1515 }
michael@0 1516
michael@0 1517 // "If new parent's nextSibling is editable and running sibling criteria on
michael@0 1518 // it returns true:"
michael@0 1519 if (isEditable(newParent.nextSibling)
michael@0 1520 && siblingCriteria(newParent.nextSibling)) {
michael@0 1521 // "If new parent is not an inline node, but new parent's last child
michael@0 1522 // and new parent's nextSibling's first child are both inline nodes,
michael@0 1523 // and new parent's last child is not a br, call createElement("br") on
michael@0 1524 // the ownerDocument of new parent and append the result as the last
michael@0 1525 // child of new parent."
michael@0 1526 if (!isInlineNode(newParent)
michael@0 1527 && isInlineNode(newParent.lastChild)
michael@0 1528 && isInlineNode(newParent.nextSibling.firstChild)
michael@0 1529 && !isHtmlElement(newParent.lastChild, "BR")) {
michael@0 1530 newParent.appendChild(newParent.ownerDocument.createElement("br"));
michael@0 1531 }
michael@0 1532
michael@0 1533 // "While new parent's nextSibling has children, append its first child
michael@0 1534 // as the last child of new parent, preserving ranges."
michael@0 1535 while (newParent.nextSibling.hasChildNodes()) {
michael@0 1536 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1);
michael@0 1537 }
michael@0 1538
michael@0 1539 // "Remove new parent's nextSibling from its parent."
michael@0 1540 newParent.parentNode.removeChild(newParent.nextSibling);
michael@0 1541 }
michael@0 1542
michael@0 1543 // "Remove extraneous line breaks from new parent."
michael@0 1544 removeExtraneousLineBreaksFrom(newParent);
michael@0 1545
michael@0 1546 // "Return new parent."
michael@0 1547 return newParent;
michael@0 1548 }
michael@0 1549
michael@0 1550
michael@0 1551 //@}
michael@0 1552 ///// Allowed children /////
michael@0 1553 //@{
michael@0 1554
michael@0 1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
michael@0 1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
michael@0 1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
michael@0 1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
michael@0 1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
michael@0 1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo",
michael@0 1561 "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
michael@0 1562 "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
michael@0 1563 "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
michael@0 1564 "xmp", "big", "blink", "font", "marquee", "nobr", "tt"];
michael@0 1565
michael@0 1566 // "An element with inline contents is an HTML element whose local name is a
michael@0 1567 // name of an element with inline contents."
michael@0 1568 function isElementWithInlineContents(node) {
michael@0 1569 return isHtmlElement(node, namesOfElementsWithInlineContents);
michael@0 1570 }
michael@0 1571
michael@0 1572 function isAllowedChild(child, parent_) {
michael@0 1573 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
michael@0 1574 // an HTML element with local name equal to one of those, and child is a
michael@0 1575 // Text node whose data does not consist solely of space characters, return
michael@0 1576 // false."
michael@0 1577 if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1
michael@0 1578 || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
michael@0 1579 && typeof child == "object"
michael@0 1580 && child.nodeType == Node.TEXT_NODE
michael@0 1581 && !/^[ \t\n\f\r]*$/.test(child.data)) {
michael@0 1582 return false;
michael@0 1583 }
michael@0 1584
michael@0 1585 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML
michael@0 1586 // element with local name equal to one of those, and child is not a Text
michael@0 1587 // node, return false."
michael@0 1588 if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1
michael@0 1589 || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"]))
michael@0 1590 && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) {
michael@0 1591 return false;
michael@0 1592 }
michael@0 1593
michael@0 1594 // "If child is a Document, DocumentFragment, or DocumentType, return
michael@0 1595 // false."
michael@0 1596 if (typeof child == "object"
michael@0 1597 && (child.nodeType == Node.DOCUMENT_NODE
michael@0 1598 || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE
michael@0 1599 || child.nodeType == Node.DOCUMENT_TYPE_NODE)) {
michael@0 1600 return false;
michael@0 1601 }
michael@0 1602
michael@0 1603 // "If child is an HTML element, set child to the local name of child."
michael@0 1604 if (isHtmlElement(child)) {
michael@0 1605 child = child.tagName.toLowerCase();
michael@0 1606 }
michael@0 1607
michael@0 1608 // "If child is not a string, return true."
michael@0 1609 if (typeof child != "string") {
michael@0 1610 return true;
michael@0 1611 }
michael@0 1612
michael@0 1613 // "If parent is an HTML element:"
michael@0 1614 if (isHtmlElement(parent_)) {
michael@0 1615 // "If child is "a", and parent or some ancestor of parent is an a,
michael@0 1616 // return false."
michael@0 1617 //
michael@0 1618 // "If child is a prohibited paragraph child name and parent or some
michael@0 1619 // ancestor of parent is an element with inline contents, return
michael@0 1620 // false."
michael@0 1621 //
michael@0 1622 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
michael@0 1623 // some ancestor of parent is an HTML element with local name "h1",
michael@0 1624 // "h2", "h3", "h4", "h5", or "h6", return false."
michael@0 1625 var ancestor = parent_;
michael@0 1626 while (ancestor) {
michael@0 1627 if (child == "a" && isHtmlElement(ancestor, "a")) {
michael@0 1628 return false;
michael@0 1629 }
michael@0 1630 if (prohibitedParagraphChildNames.indexOf(child) != -1
michael@0 1631 && isElementWithInlineContents(ancestor)) {
michael@0 1632 return false;
michael@0 1633 }
michael@0 1634 if (/^h[1-6]$/.test(child)
michael@0 1635 && isHtmlElement(ancestor)
michael@0 1636 && /^H[1-6]$/.test(ancestor.tagName)) {
michael@0 1637 return false;
michael@0 1638 }
michael@0 1639 ancestor = ancestor.parentNode;
michael@0 1640 }
michael@0 1641
michael@0 1642 // "Let parent be the local name of parent."
michael@0 1643 parent_ = parent_.tagName.toLowerCase();
michael@0 1644 }
michael@0 1645
michael@0 1646 // "If parent is an Element or DocumentFragment, return true."
michael@0 1647 if (typeof parent_ == "object"
michael@0 1648 && (parent_.nodeType == Node.ELEMENT_NODE
michael@0 1649 || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
michael@0 1650 return true;
michael@0 1651 }
michael@0 1652
michael@0 1653 // "If parent is not a string, return false."
michael@0 1654 if (typeof parent_ != "string") {
michael@0 1655 return false;
michael@0 1656 }
michael@0 1657
michael@0 1658 // "If parent is on the left-hand side of an entry on the following list,
michael@0 1659 // then return true if child is listed on the right-hand side of that
michael@0 1660 // entry, and false otherwise."
michael@0 1661 switch (parent_) {
michael@0 1662 case "colgroup":
michael@0 1663 return child == "col";
michael@0 1664 case "table":
michael@0 1665 return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1;
michael@0 1666 case "tbody":
michael@0 1667 case "thead":
michael@0 1668 case "tfoot":
michael@0 1669 return ["td", "th", "tr"].indexOf(child) != -1;
michael@0 1670 case "tr":
michael@0 1671 return ["td", "th"].indexOf(child) != -1;
michael@0 1672 case "dl":
michael@0 1673 return ["dt", "dd"].indexOf(child) != -1;
michael@0 1674 case "dir":
michael@0 1675 case "ol":
michael@0 1676 case "ul":
michael@0 1677 return ["dir", "li", "ol", "ul"].indexOf(child) != -1;
michael@0 1678 case "hgroup":
michael@0 1679 return /^h[1-6]$/.test(child);
michael@0 1680 }
michael@0 1681
michael@0 1682 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
michael@0 1683 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
michael@0 1684 // false."
michael@0 1685 if (["body", "caption", "col", "colgroup", "frame", "frameset", "head",
michael@0 1686 "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) {
michael@0 1687 return false;
michael@0 1688 }
michael@0 1689
michael@0 1690 // "If child is "dd" or "dt" and parent is not "dl", return false."
michael@0 1691 if (["dd", "dt"].indexOf(child) != -1
michael@0 1692 && parent_ != "dl") {
michael@0 1693 return false;
michael@0 1694 }
michael@0 1695
michael@0 1696 // "If child is "li" and parent is not "ol" or "ul", return false."
michael@0 1697 if (child == "li"
michael@0 1698 && parent_ != "ol"
michael@0 1699 && parent_ != "ul") {
michael@0 1700 return false;
michael@0 1701 }
michael@0 1702
michael@0 1703 // "If parent is on the left-hand side of an entry on the following list
michael@0 1704 // and child is listed on the right-hand side of that entry, return false."
michael@0 1705 var table = [
michael@0 1706 [["a"], ["a"]],
michael@0 1707 [["dd", "dt"], ["dd", "dt"]],
michael@0 1708 [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]],
michael@0 1709 [["li"], ["li"]],
michael@0 1710 [["nobr"], ["nobr"]],
michael@0 1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
michael@0 1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]],
michael@0 1713 ];
michael@0 1714 for (var i = 0; i < table.length; i++) {
michael@0 1715 if (table[i][0].indexOf(parent_) != -1
michael@0 1716 && table[i][1].indexOf(child) != -1) {
michael@0 1717 return false;
michael@0 1718 }
michael@0 1719 }
michael@0 1720
michael@0 1721 // "Return true."
michael@0 1722 return true;
michael@0 1723 }
michael@0 1724
michael@0 1725
michael@0 1726 //@}
michael@0 1727
michael@0 1728 //////////////////////////////////////
michael@0 1729 ///// Inline formatting commands /////
michael@0 1730 //////////////////////////////////////
michael@0 1731
michael@0 1732 ///// Inline formatting command definitions /////
michael@0 1733 //@{
michael@0 1734
michael@0 1735 // "A node node is effectively contained in a range range if range is not
michael@0 1736 // collapsed, and at least one of the following holds:"
michael@0 1737 function isEffectivelyContained(node, range) {
michael@0 1738 if (range.collapsed) {
michael@0 1739 return false;
michael@0 1740 }
michael@0 1741
michael@0 1742 // "node is contained in range."
michael@0 1743 if (isContained(node, range)) {
michael@0 1744 return true;
michael@0 1745 }
michael@0 1746
michael@0 1747 // "node is range's start node, it is a Text node, and its length is
michael@0 1748 // different from range's start offset."
michael@0 1749 if (node == range.startContainer
michael@0 1750 && node.nodeType == Node.TEXT_NODE
michael@0 1751 && getNodeLength(node) != range.startOffset) {
michael@0 1752 return true;
michael@0 1753 }
michael@0 1754
michael@0 1755 // "node is range's end node, it is a Text node, and range's end offset is
michael@0 1756 // not 0."
michael@0 1757 if (node == range.endContainer
michael@0 1758 && node.nodeType == Node.TEXT_NODE
michael@0 1759 && range.endOffset != 0) {
michael@0 1760 return true;
michael@0 1761 }
michael@0 1762
michael@0 1763 // "node has at least one child; and all its children are effectively
michael@0 1764 // contained in range; and either range's start node is not a descendant of
michael@0 1765 // node or is not a Text node or range's start offset is zero; and either
michael@0 1766 // range's end node is not a descendant of node or is not a Text node or
michael@0 1767 // range's end offset is its end node's length."
michael@0 1768 if (node.hasChildNodes()
michael@0 1769 && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) })
michael@0 1770 && (!isDescendant(range.startContainer, node)
michael@0 1771 || range.startContainer.nodeType != Node.TEXT_NODE
michael@0 1772 || range.startOffset == 0)
michael@0 1773 && (!isDescendant(range.endContainer, node)
michael@0 1774 || range.endContainer.nodeType != Node.TEXT_NODE
michael@0 1775 || range.endOffset == getNodeLength(range.endContainer))) {
michael@0 1776 return true;
michael@0 1777 }
michael@0 1778
michael@0 1779 return false;
michael@0 1780 }
michael@0 1781
michael@0 1782 // Like get(All)ContainedNodes(), but for effectively contained nodes.
michael@0 1783 function getEffectivelyContainedNodes(range, condition) {
michael@0 1784 if (typeof condition == "undefined") {
michael@0 1785 condition = function() { return true };
michael@0 1786 }
michael@0 1787 var node = range.startContainer;
michael@0 1788 while (isEffectivelyContained(node.parentNode, range)) {
michael@0 1789 node = node.parentNode;
michael@0 1790 }
michael@0 1791
michael@0 1792 var stop = nextNodeDescendants(range.endContainer);
michael@0 1793
michael@0 1794 var nodeList = [];
michael@0 1795 while (isBefore(node, stop)) {
michael@0 1796 if (isEffectivelyContained(node, range)
michael@0 1797 && condition(node)) {
michael@0 1798 nodeList.push(node);
michael@0 1799 node = nextNodeDescendants(node);
michael@0 1800 continue;
michael@0 1801 }
michael@0 1802 node = nextNode(node);
michael@0 1803 }
michael@0 1804 return nodeList;
michael@0 1805 }
michael@0 1806
michael@0 1807 function getAllEffectivelyContainedNodes(range, condition) {
michael@0 1808 if (typeof condition == "undefined") {
michael@0 1809 condition = function() { return true };
michael@0 1810 }
michael@0 1811 var node = range.startContainer;
michael@0 1812 while (isEffectivelyContained(node.parentNode, range)) {
michael@0 1813 node = node.parentNode;
michael@0 1814 }
michael@0 1815
michael@0 1816 var stop = nextNodeDescendants(range.endContainer);
michael@0 1817
michael@0 1818 var nodeList = [];
michael@0 1819 while (isBefore(node, stop)) {
michael@0 1820 if (isEffectivelyContained(node, range)
michael@0 1821 && condition(node)) {
michael@0 1822 nodeList.push(node);
michael@0 1823 }
michael@0 1824 node = nextNode(node);
michael@0 1825 }
michael@0 1826 return nodeList;
michael@0 1827 }
michael@0 1828
michael@0 1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
michael@0 1830 // with no attributes except possibly style; or a font element with no
michael@0 1831 // attributes except possibly style, color, face, and/or size; or an a element
michael@0 1832 // with no attributes except possibly style and/or href."
michael@0 1833 function isModifiableElement(node) {
michael@0 1834 if (!isHtmlElement(node)) {
michael@0 1835 return false;
michael@0 1836 }
michael@0 1837
michael@0 1838 if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) {
michael@0 1839 if (node.attributes.length == 0) {
michael@0 1840 return true;
michael@0 1841 }
michael@0 1842
michael@0 1843 if (node.attributes.length == 1
michael@0 1844 && node.hasAttribute("style")) {
michael@0 1845 return true;
michael@0 1846 }
michael@0 1847 }
michael@0 1848
michael@0 1849 if (node.tagName == "FONT" || node.tagName == "A") {
michael@0 1850 var numAttrs = node.attributes.length;
michael@0 1851
michael@0 1852 if (node.hasAttribute("style")) {
michael@0 1853 numAttrs--;
michael@0 1854 }
michael@0 1855
michael@0 1856 if (node.tagName == "FONT") {
michael@0 1857 if (node.hasAttribute("color")) {
michael@0 1858 numAttrs--;
michael@0 1859 }
michael@0 1860
michael@0 1861 if (node.hasAttribute("face")) {
michael@0 1862 numAttrs--;
michael@0 1863 }
michael@0 1864
michael@0 1865 if (node.hasAttribute("size")) {
michael@0 1866 numAttrs--;
michael@0 1867 }
michael@0 1868 }
michael@0 1869
michael@0 1870 if (node.tagName == "A"
michael@0 1871 && node.hasAttribute("href")) {
michael@0 1872 numAttrs--;
michael@0 1873 }
michael@0 1874
michael@0 1875 if (numAttrs == 0) {
michael@0 1876 return true;
michael@0 1877 }
michael@0 1878 }
michael@0 1879
michael@0 1880 return false;
michael@0 1881 }
michael@0 1882
michael@0 1883 function isSimpleModifiableElement(node) {
michael@0 1884 // "A simple modifiable element is an HTML element for which at least one
michael@0 1885 // of the following holds:"
michael@0 1886 if (!isHtmlElement(node)) {
michael@0 1887 return false;
michael@0 1888 }
michael@0 1889
michael@0 1890 // Only these elements can possibly be a simple modifiable element.
michael@0 1891 if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) {
michael@0 1892 return false;
michael@0 1893 }
michael@0 1894
michael@0 1895 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
michael@0 1896 // element with no attributes."
michael@0 1897 if (node.attributes.length == 0) {
michael@0 1898 return true;
michael@0 1899 }
michael@0 1900
michael@0 1901 // If it's got more than one attribute, everything after this fails.
michael@0 1902 if (node.attributes.length > 1) {
michael@0 1903 return false;
michael@0 1904 }
michael@0 1905
michael@0 1906 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
michael@0 1907 // element with exactly one attribute, which is style, which sets no CSS
michael@0 1908 // properties (including invalid or unrecognized properties)."
michael@0 1909 //
michael@0 1910 // Not gonna try for invalid or unrecognized.
michael@0 1911 if (node.hasAttribute("style")
michael@0 1912 && node.style.length == 0) {
michael@0 1913 return true;
michael@0 1914 }
michael@0 1915
michael@0 1916 // "It is an a element with exactly one attribute, which is href."
michael@0 1917 if (node.tagName == "A"
michael@0 1918 && node.hasAttribute("href")) {
michael@0 1919 return true;
michael@0 1920 }
michael@0 1921
michael@0 1922 // "It is a font element with exactly one attribute, which is either color,
michael@0 1923 // face, or size."
michael@0 1924 if (node.tagName == "FONT"
michael@0 1925 && (node.hasAttribute("color")
michael@0 1926 || node.hasAttribute("face")
michael@0 1927 || node.hasAttribute("size")
michael@0 1928 )) {
michael@0 1929 return true;
michael@0 1930 }
michael@0 1931
michael@0 1932 // "It is a b or strong element with exactly one attribute, which is style,
michael@0 1933 // and the style attribute sets exactly one CSS property (including invalid
michael@0 1934 // or unrecognized properties), which is "font-weight"."
michael@0 1935 if ((node.tagName == "B" || node.tagName == "STRONG")
michael@0 1936 && node.hasAttribute("style")
michael@0 1937 && node.style.length == 1
michael@0 1938 && node.style.fontWeight != "") {
michael@0 1939 return true;
michael@0 1940 }
michael@0 1941
michael@0 1942 // "It is an i or em element with exactly one attribute, which is style,
michael@0 1943 // and the style attribute sets exactly one CSS property (including invalid
michael@0 1944 // or unrecognized properties), which is "font-style"."
michael@0 1945 if ((node.tagName == "I" || node.tagName == "EM")
michael@0 1946 && node.hasAttribute("style")
michael@0 1947 && node.style.length == 1
michael@0 1948 && node.style.fontStyle != "") {
michael@0 1949 return true;
michael@0 1950 }
michael@0 1951
michael@0 1952 // "It is an a, font, or span element with exactly one attribute, which is
michael@0 1953 // style, and the style attribute sets exactly one CSS property (including
michael@0 1954 // invalid or unrecognized properties), and that property is not
michael@0 1955 // "text-decoration"."
michael@0 1956 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
michael@0 1957 && node.hasAttribute("style")
michael@0 1958 && node.style.length == 1
michael@0 1959 && node.style.textDecoration == "") {
michael@0 1960 return true;
michael@0 1961 }
michael@0 1962
michael@0 1963 // "It is an a, font, s, span, strike, or u element with exactly one
michael@0 1964 // attribute, which is style, and the style attribute sets exactly one CSS
michael@0 1965 // property (including invalid or unrecognized properties), which is
michael@0 1966 // "text-decoration", which is set to "line-through" or "underline" or
michael@0 1967 // "overline" or "none"."
michael@0 1968 //
michael@0 1969 // The weird extra node.style.length check is for Firefox, which as of
michael@0 1970 // 8.0a2 has annoying and weird behavior here.
michael@0 1971 if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1
michael@0 1972 && node.hasAttribute("style")
michael@0 1973 && (node.style.length == 1
michael@0 1974 || (node.style.length == 4
michael@0 1975 && "MozTextBlink" in node.style
michael@0 1976 && "MozTextDecorationColor" in node.style
michael@0 1977 && "MozTextDecorationLine" in node.style
michael@0 1978 && "MozTextDecorationStyle" in node.style)
michael@0 1979 )
michael@0 1980 && (node.style.textDecoration == "line-through"
michael@0 1981 || node.style.textDecoration == "underline"
michael@0 1982 || node.style.textDecoration == "overline"
michael@0 1983 || node.style.textDecoration == "none")) {
michael@0 1984 return true;
michael@0 1985 }
michael@0 1986
michael@0 1987 return false;
michael@0 1988 }
michael@0 1989
michael@0 1990 // "A formattable node is an editable visible node that is either a Text node,
michael@0 1991 // an img, or a br."
michael@0 1992 function isFormattableNode(node) {
michael@0 1993 return isEditable(node)
michael@0 1994 && isVisible(node)
michael@0 1995 && (node.nodeType == Node.TEXT_NODE
michael@0 1996 || isHtmlElement(node, ["img", "br"]));
michael@0 1997 }
michael@0 1998
michael@0 1999 // "Two quantities are equivalent values for a command if either both are null,
michael@0 2000 // or both are strings and they're equal and the command does not define any
michael@0 2001 // equivalent values, or both are strings and the command defines equivalent
michael@0 2002 // values and they match the definition."
michael@0 2003 function areEquivalentValues(command, val1, val2) {
michael@0 2004 if (val1 === null && val2 === null) {
michael@0 2005 return true;
michael@0 2006 }
michael@0 2007
michael@0 2008 if (typeof val1 == "string"
michael@0 2009 && typeof val2 == "string"
michael@0 2010 && val1 == val2
michael@0 2011 && !("equivalentValues" in commands[command])) {
michael@0 2012 return true;
michael@0 2013 }
michael@0 2014
michael@0 2015 if (typeof val1 == "string"
michael@0 2016 && typeof val2 == "string"
michael@0 2017 && "equivalentValues" in commands[command]
michael@0 2018 && commands[command].equivalentValues(val1, val2)) {
michael@0 2019 return true;
michael@0 2020 }
michael@0 2021
michael@0 2022 return false;
michael@0 2023 }
michael@0 2024
michael@0 2025 // "Two quantities are loosely equivalent values for a command if either they
michael@0 2026 // are equivalent values for the command, or if the command is the fontSize
michael@0 2027 // command; one of the quantities is one of "x-small", "small", "medium",
michael@0 2028 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
michael@0 2029 // the resolved value of "font-size" on a font element whose size attribute has
michael@0 2030 // the corresponding value set ("1" through "7" respectively)."
michael@0 2031 function areLooselyEquivalentValues(command, val1, val2) {
michael@0 2032 if (areEquivalentValues(command, val1, val2)) {
michael@0 2033 return true;
michael@0 2034 }
michael@0 2035
michael@0 2036 if (command != "fontsize"
michael@0 2037 || typeof val1 != "string"
michael@0 2038 || typeof val2 != "string") {
michael@0 2039 return false;
michael@0 2040 }
michael@0 2041
michael@0 2042 // Static variables in JavaScript?
michael@0 2043 var callee = areLooselyEquivalentValues;
michael@0 2044 if (callee.sizeMap === undefined) {
michael@0 2045 callee.sizeMap = {};
michael@0 2046 var font = document.createElement("font");
michael@0 2047 document.body.appendChild(font);
michael@0 2048 ["x-small", "small", "medium", "large", "x-large", "xx-large",
michael@0 2049 "xxx-large"].forEach(function(keyword) {
michael@0 2050 font.size = cssSizeToLegacy(keyword);
michael@0 2051 callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
michael@0 2052 });
michael@0 2053 document.body.removeChild(font);
michael@0 2054 }
michael@0 2055
michael@0 2056 return val1 === callee.sizeMap[val2]
michael@0 2057 || val2 === callee.sizeMap[val1];
michael@0 2058 }
michael@0 2059
michael@0 2060 //@}
michael@0 2061 ///// Assorted inline formatting command algorithms /////
michael@0 2062 //@{
michael@0 2063
michael@0 2064 function getEffectiveCommandValue(node, command) {
michael@0 2065 // "If neither node nor its parent is an Element, return null."
michael@0 2066 if (node.nodeType != Node.ELEMENT_NODE
michael@0 2067 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
michael@0 2068 return null;
michael@0 2069 }
michael@0 2070
michael@0 2071 // "If node is not an Element, return the effective command value of its
michael@0 2072 // parent for command."
michael@0 2073 if (node.nodeType != Node.ELEMENT_NODE) {
michael@0 2074 return getEffectiveCommandValue(node.parentNode, command);
michael@0 2075 }
michael@0 2076
michael@0 2077 // "If command is "createLink" or "unlink":"
michael@0 2078 if (command == "createlink" || command == "unlink") {
michael@0 2079 // "While node is not null, and is not an a element that has an href
michael@0 2080 // attribute, set node to its parent."
michael@0 2081 while (node
michael@0 2082 && (!isHtmlElement(node)
michael@0 2083 || node.tagName != "A"
michael@0 2084 || !node.hasAttribute("href"))) {
michael@0 2085 node = node.parentNode;
michael@0 2086 }
michael@0 2087
michael@0 2088 // "If node is null, return null."
michael@0 2089 if (!node) {
michael@0 2090 return null;
michael@0 2091 }
michael@0 2092
michael@0 2093 // "Return the value of node's href attribute."
michael@0 2094 return node.getAttribute("href");
michael@0 2095 }
michael@0 2096
michael@0 2097 // "If command is "backColor" or "hiliteColor":"
michael@0 2098 if (command == "backcolor"
michael@0 2099 || command == "hilitecolor") {
michael@0 2100 // "While the resolved value of "background-color" on node is any
michael@0 2101 // fully transparent value, and node's parent is an Element, set
michael@0 2102 // node to its parent."
michael@0 2103 //
michael@0 2104 // Another lame hack to avoid flawed APIs.
michael@0 2105 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
michael@0 2106 || getComputedStyle(node).backgroundColor === ""
michael@0 2107 || getComputedStyle(node).backgroundColor == "transparent")
michael@0 2108 && node.parentNode
michael@0 2109 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
michael@0 2110 node = node.parentNode;
michael@0 2111 }
michael@0 2112
michael@0 2113 // "Return the resolved value of "background-color" for node."
michael@0 2114 return getComputedStyle(node).backgroundColor;
michael@0 2115 }
michael@0 2116
michael@0 2117 // "If command is "subscript" or "superscript":"
michael@0 2118 if (command == "subscript" || command == "superscript") {
michael@0 2119 // "Let affected by subscript and affected by superscript be two
michael@0 2120 // boolean variables, both initially false."
michael@0 2121 var affectedBySubscript = false;
michael@0 2122 var affectedBySuperscript = false;
michael@0 2123
michael@0 2124 // "While node is an inline node:"
michael@0 2125 while (isInlineNode(node)) {
michael@0 2126 var verticalAlign = getComputedStyle(node).verticalAlign;
michael@0 2127
michael@0 2128 // "If node is a sub, set affected by subscript to true."
michael@0 2129 if (isHtmlElement(node, "sub")) {
michael@0 2130 affectedBySubscript = true;
michael@0 2131 // "Otherwise, if node is a sup, set affected by superscript to
michael@0 2132 // true."
michael@0 2133 } else if (isHtmlElement(node, "sup")) {
michael@0 2134 affectedBySuperscript = true;
michael@0 2135 }
michael@0 2136
michael@0 2137 // "Set node to its parent."
michael@0 2138 node = node.parentNode;
michael@0 2139 }
michael@0 2140
michael@0 2141 // "If affected by subscript and affected by superscript are both true,
michael@0 2142 // return the string "mixed"."
michael@0 2143 if (affectedBySubscript && affectedBySuperscript) {
michael@0 2144 return "mixed";
michael@0 2145 }
michael@0 2146
michael@0 2147 // "If affected by subscript is true, return "subscript"."
michael@0 2148 if (affectedBySubscript) {
michael@0 2149 return "subscript";
michael@0 2150 }
michael@0 2151
michael@0 2152 // "If affected by superscript is true, return "superscript"."
michael@0 2153 if (affectedBySuperscript) {
michael@0 2154 return "superscript";
michael@0 2155 }
michael@0 2156
michael@0 2157 // "Return null."
michael@0 2158 return null;
michael@0 2159 }
michael@0 2160
michael@0 2161 // "If command is "strikethrough", and the "text-decoration" property of
michael@0 2162 // node or any of its ancestors has resolved value containing
michael@0 2163 // "line-through", return "line-through". Otherwise, return null."
michael@0 2164 if (command == "strikethrough") {
michael@0 2165 do {
michael@0 2166 if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
michael@0 2167 return "line-through";
michael@0 2168 }
michael@0 2169 node = node.parentNode;
michael@0 2170 } while (node && node.nodeType == Node.ELEMENT_NODE);
michael@0 2171 return null;
michael@0 2172 }
michael@0 2173
michael@0 2174 // "If command is "underline", and the "text-decoration" property of node
michael@0 2175 // or any of its ancestors has resolved value containing "underline",
michael@0 2176 // return "underline". Otherwise, return null."
michael@0 2177 if (command == "underline") {
michael@0 2178 do {
michael@0 2179 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
michael@0 2180 return "underline";
michael@0 2181 }
michael@0 2182 node = node.parentNode;
michael@0 2183 } while (node && node.nodeType == Node.ELEMENT_NODE);
michael@0 2184 return null;
michael@0 2185 }
michael@0 2186
michael@0 2187 if (!("relevantCssProperty" in commands[command])) {
michael@0 2188 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
michael@0 2189 }
michael@0 2190
michael@0 2191 // "Return the resolved value for node of the relevant CSS property for
michael@0 2192 // command."
michael@0 2193 return getComputedStyle(node)[commands[command].relevantCssProperty];
michael@0 2194 }
michael@0 2195
michael@0 2196 function getSpecifiedCommandValue(element, command) {
michael@0 2197 // "If command is "backColor" or "hiliteColor" and element's display
michael@0 2198 // property does not have resolved value "inline", return null."
michael@0 2199 if ((command == "backcolor" || command == "hilitecolor")
michael@0 2200 && getComputedStyle(element).display != "inline") {
michael@0 2201 return null;
michael@0 2202 }
michael@0 2203
michael@0 2204 // "If command is "createLink" or "unlink":"
michael@0 2205 if (command == "createlink" || command == "unlink") {
michael@0 2206 // "If element is an a element and has an href attribute, return the
michael@0 2207 // value of that attribute."
michael@0 2208 if (isHtmlElement(element)
michael@0 2209 && element.tagName == "A"
michael@0 2210 && element.hasAttribute("href")) {
michael@0 2211 return element.getAttribute("href");
michael@0 2212 }
michael@0 2213
michael@0 2214 // "Return null."
michael@0 2215 return null;
michael@0 2216 }
michael@0 2217
michael@0 2218 // "If command is "subscript" or "superscript":"
michael@0 2219 if (command == "subscript" || command == "superscript") {
michael@0 2220 // "If element is a sup, return "superscript"."
michael@0 2221 if (isHtmlElement(element, "sup")) {
michael@0 2222 return "superscript";
michael@0 2223 }
michael@0 2224
michael@0 2225 // "If element is a sub, return "subscript"."
michael@0 2226 if (isHtmlElement(element, "sub")) {
michael@0 2227 return "subscript";
michael@0 2228 }
michael@0 2229
michael@0 2230 // "Return null."
michael@0 2231 return null;
michael@0 2232 }
michael@0 2233
michael@0 2234 // "If command is "strikethrough", and element has a style attribute set,
michael@0 2235 // and that attribute sets "text-decoration":"
michael@0 2236 if (command == "strikethrough"
michael@0 2237 && element.style.textDecoration != "") {
michael@0 2238 // "If element's style attribute sets "text-decoration" to a value
michael@0 2239 // containing "line-through", return "line-through"."
michael@0 2240 if (element.style.textDecoration.indexOf("line-through") != -1) {
michael@0 2241 return "line-through";
michael@0 2242 }
michael@0 2243
michael@0 2244 // "Return null."
michael@0 2245 return null;
michael@0 2246 }
michael@0 2247
michael@0 2248 // "If command is "strikethrough" and element is a s or strike element,
michael@0 2249 // return "line-through"."
michael@0 2250 if (command == "strikethrough"
michael@0 2251 && isHtmlElement(element, ["S", "STRIKE"])) {
michael@0 2252 return "line-through";
michael@0 2253 }
michael@0 2254
michael@0 2255 // "If command is "underline", and element has a style attribute set, and
michael@0 2256 // that attribute sets "text-decoration":"
michael@0 2257 if (command == "underline"
michael@0 2258 && element.style.textDecoration != "") {
michael@0 2259 // "If element's style attribute sets "text-decoration" to a value
michael@0 2260 // containing "underline", return "underline"."
michael@0 2261 if (element.style.textDecoration.indexOf("underline") != -1) {
michael@0 2262 return "underline";
michael@0 2263 }
michael@0 2264
michael@0 2265 // "Return null."
michael@0 2266 return null;
michael@0 2267 }
michael@0 2268
michael@0 2269 // "If command is "underline" and element is a u element, return
michael@0 2270 // "underline"."
michael@0 2271 if (command == "underline"
michael@0 2272 && isHtmlElement(element, "U")) {
michael@0 2273 return "underline";
michael@0 2274 }
michael@0 2275
michael@0 2276 // "Let property be the relevant CSS property for command."
michael@0 2277 var property = commands[command].relevantCssProperty;
michael@0 2278
michael@0 2279 // "If property is null, return null."
michael@0 2280 if (property === null) {
michael@0 2281 return null;
michael@0 2282 }
michael@0 2283
michael@0 2284 // "If element has a style attribute set, and that attribute has the
michael@0 2285 // effect of setting property, return the value that it sets property to."
michael@0 2286 if (element.style[property] != "") {
michael@0 2287 return element.style[property];
michael@0 2288 }
michael@0 2289
michael@0 2290 // "If element is a font element that has an attribute whose effect is
michael@0 2291 // to create a presentational hint for property, return the value that the
michael@0 2292 // hint sets property to. (For a size of 7, this will be the non-CSS value
michael@0 2293 // "xxx-large".)"
michael@0 2294 if (isHtmlNamespace(element.namespaceURI)
michael@0 2295 && element.tagName == "FONT") {
michael@0 2296 if (property == "color" && element.hasAttribute("color")) {
michael@0 2297 return element.color;
michael@0 2298 }
michael@0 2299 if (property == "fontFamily" && element.hasAttribute("face")) {
michael@0 2300 return element.face;
michael@0 2301 }
michael@0 2302 if (property == "fontSize" && element.hasAttribute("size")) {
michael@0 2303 // This is not even close to correct in general.
michael@0 2304 var size = parseInt(element.size);
michael@0 2305 if (size < 1) {
michael@0 2306 size = 1;
michael@0 2307 }
michael@0 2308 if (size > 7) {
michael@0 2309 size = 7;
michael@0 2310 }
michael@0 2311 return {
michael@0 2312 1: "x-small",
michael@0 2313 2: "small",
michael@0 2314 3: "medium",
michael@0 2315 4: "large",
michael@0 2316 5: "x-large",
michael@0 2317 6: "xx-large",
michael@0 2318 7: "xxx-large"
michael@0 2319 }[size];
michael@0 2320 }
michael@0 2321 }
michael@0 2322
michael@0 2323 // "If element is in the following list, and property is equal to the
michael@0 2324 // CSS property name listed for it, return the string listed for it."
michael@0 2325 //
michael@0 2326 // A list follows, whose meaning is copied here.
michael@0 2327 if (property == "fontWeight"
michael@0 2328 && (element.tagName == "B" || element.tagName == "STRONG")) {
michael@0 2329 return "bold";
michael@0 2330 }
michael@0 2331 if (property == "fontStyle"
michael@0 2332 && (element.tagName == "I" || element.tagName == "EM")) {
michael@0 2333 return "italic";
michael@0 2334 }
michael@0 2335
michael@0 2336 // "Return null."
michael@0 2337 return null;
michael@0 2338 }
michael@0 2339
michael@0 2340 function reorderModifiableDescendants(node, command, newValue) {
michael@0 2341 // "Let candidate equal node."
michael@0 2342 var candidate = node;
michael@0 2343
michael@0 2344 // "While candidate is a modifiable element, and candidate has exactly one
michael@0 2345 // child, and that child is also a modifiable element, and candidate is not
michael@0 2346 // a simple modifiable element or candidate's specified command value for
michael@0 2347 // command is not equivalent to new value, set candidate to its child."
michael@0 2348 while (isModifiableElement(candidate)
michael@0 2349 && candidate.childNodes.length == 1
michael@0 2350 && isModifiableElement(candidate.firstChild)
michael@0 2351 && (!isSimpleModifiableElement(candidate)
michael@0 2352 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
michael@0 2353 candidate = candidate.firstChild;
michael@0 2354 }
michael@0 2355
michael@0 2356 // "If candidate is node, or is not a simple modifiable element, or its
michael@0 2357 // specified command value is not equivalent to new value, or its effective
michael@0 2358 // command value is not loosely equivalent to new value, abort these
michael@0 2359 // steps."
michael@0 2360 if (candidate == node
michael@0 2361 || !isSimpleModifiableElement(candidate)
michael@0 2362 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
michael@0 2363 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
michael@0 2364 return;
michael@0 2365 }
michael@0 2366
michael@0 2367 // "While candidate has children, insert the first child of candidate into
michael@0 2368 // candidate's parent immediately before candidate, preserving ranges."
michael@0 2369 while (candidate.hasChildNodes()) {
michael@0 2370 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate));
michael@0 2371 }
michael@0 2372
michael@0 2373 // "Insert candidate into node's parent immediately after node."
michael@0 2374 node.parentNode.insertBefore(candidate, node.nextSibling);
michael@0 2375
michael@0 2376 // "Append the node as the last child of candidate, preserving ranges."
michael@0 2377 movePreservingRanges(node, candidate, -1);
michael@0 2378 }
michael@0 2379
michael@0 2380 function recordValues(nodeList) {
michael@0 2381 // "Let values be a list of (node, command, specified command value)
michael@0 2382 // triples, initially empty."
michael@0 2383 var values = [];
michael@0 2384
michael@0 2385 // "For each node in node list, for each command in the list "subscript",
michael@0 2386 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
michael@0 2387 // "strikethrough", and "underline" in that order:"
michael@0 2388 nodeList.forEach(function(node) {
michael@0 2389 ["subscript", "bold", "fontname", "fontsize", "forecolor",
michael@0 2390 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) {
michael@0 2391 // "Let ancestor equal node."
michael@0 2392 var ancestor = node;
michael@0 2393
michael@0 2394 // "If ancestor is not an Element, set it to its parent."
michael@0 2395 if (ancestor.nodeType != Node.ELEMENT_NODE) {
michael@0 2396 ancestor = ancestor.parentNode;
michael@0 2397 }
michael@0 2398
michael@0 2399 // "While ancestor is an Element and its specified command value
michael@0 2400 // for command is null, set it to its parent."
michael@0 2401 while (ancestor
michael@0 2402 && ancestor.nodeType == Node.ELEMENT_NODE
michael@0 2403 && getSpecifiedCommandValue(ancestor, command) === null) {
michael@0 2404 ancestor = ancestor.parentNode;
michael@0 2405 }
michael@0 2406
michael@0 2407 // "If ancestor is an Element, add (node, command, ancestor's
michael@0 2408 // specified command value for command) to values. Otherwise add
michael@0 2409 // (node, command, null) to values."
michael@0 2410 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
michael@0 2411 values.push([node, command, getSpecifiedCommandValue(ancestor, command)]);
michael@0 2412 } else {
michael@0 2413 values.push([node, command, null]);
michael@0 2414 }
michael@0 2415 });
michael@0 2416 });
michael@0 2417
michael@0 2418 // "Return values."
michael@0 2419 return values;
michael@0 2420 }
michael@0 2421
michael@0 2422 function restoreValues(values) {
michael@0 2423 // "For each (node, command, value) triple in values:"
michael@0 2424 values.forEach(function(triple) {
michael@0 2425 var node = triple[0];
michael@0 2426 var command = triple[1];
michael@0 2427 var value = triple[2];
michael@0 2428
michael@0 2429 // "Let ancestor equal node."
michael@0 2430 var ancestor = node;
michael@0 2431
michael@0 2432 // "If ancestor is not an Element, set it to its parent."
michael@0 2433 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
michael@0 2434 ancestor = ancestor.parentNode;
michael@0 2435 }
michael@0 2436
michael@0 2437 // "While ancestor is an Element and its specified command value for
michael@0 2438 // command is null, set it to its parent."
michael@0 2439 while (ancestor
michael@0 2440 && ancestor.nodeType == Node.ELEMENT_NODE
michael@0 2441 && getSpecifiedCommandValue(ancestor, command) === null) {
michael@0 2442 ancestor = ancestor.parentNode;
michael@0 2443 }
michael@0 2444
michael@0 2445 // "If value is null and ancestor is an Element, push down values on
michael@0 2446 // node for command, with new value null."
michael@0 2447 if (value === null
michael@0 2448 && ancestor
michael@0 2449 && ancestor.nodeType == Node.ELEMENT_NODE) {
michael@0 2450 pushDownValues(node, command, null);
michael@0 2451
michael@0 2452 // "Otherwise, if ancestor is an Element and its specified command
michael@0 2453 // value for command is not equivalent to value, or if ancestor is not
michael@0 2454 // an Element and value is not null, force the value of command to
michael@0 2455 // value on node."
michael@0 2456 } else if ((ancestor
michael@0 2457 && ancestor.nodeType == Node.ELEMENT_NODE
michael@0 2458 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
michael@0 2459 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
michael@0 2460 && value !== null)) {
michael@0 2461 forceValue(node, command, value);
michael@0 2462 }
michael@0 2463 });
michael@0 2464 }
michael@0 2465
michael@0 2466
michael@0 2467 //@}
michael@0 2468 ///// Clearing an element's value /////
michael@0 2469 //@{
michael@0 2470
michael@0 2471 function clearValue(element, command) {
michael@0 2472 // "If element is not editable, return the empty list."
michael@0 2473 if (!isEditable(element)) {
michael@0 2474 return [];
michael@0 2475 }
michael@0 2476
michael@0 2477 // "If element's specified command value for command is null, return the
michael@0 2478 // empty list."
michael@0 2479 if (getSpecifiedCommandValue(element, command) === null) {
michael@0 2480 return [];
michael@0 2481 }
michael@0 2482
michael@0 2483 // "If element is a simple modifiable element:"
michael@0 2484 if (isSimpleModifiableElement(element)) {
michael@0 2485 // "Let children be the children of element."
michael@0 2486 var children = Array.prototype.slice.call(element.childNodes);
michael@0 2487
michael@0 2488 // "For each child in children, insert child into element's parent
michael@0 2489 // immediately before element, preserving ranges."
michael@0 2490 for (var i = 0; i < children.length; i++) {
michael@0 2491 movePreservingRanges(children[i], element.parentNode, getNodeIndex(element));
michael@0 2492 }
michael@0 2493
michael@0 2494 // "Remove element from its parent."
michael@0 2495 element.parentNode.removeChild(element);
michael@0 2496
michael@0 2497 // "Return children."
michael@0 2498 return children;
michael@0 2499 }
michael@0 2500
michael@0 2501 // "If command is "strikethrough", and element has a style attribute that
michael@0 2502 // sets "text-decoration" to some value containing "line-through", delete
michael@0 2503 // "line-through" from the value."
michael@0 2504 if (command == "strikethrough"
michael@0 2505 && element.style.textDecoration.indexOf("line-through") != -1) {
michael@0 2506 if (element.style.textDecoration == "line-through") {
michael@0 2507 element.style.textDecoration = "";
michael@0 2508 } else {
michael@0 2509 element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
michael@0 2510 }
michael@0 2511 if (element.getAttribute("style") == "") {
michael@0 2512 element.removeAttribute("style");
michael@0 2513 }
michael@0 2514 }
michael@0 2515
michael@0 2516 // "If command is "underline", and element has a style attribute that sets
michael@0 2517 // "text-decoration" to some value containing "underline", delete
michael@0 2518 // "underline" from the value."
michael@0 2519 if (command == "underline"
michael@0 2520 && element.style.textDecoration.indexOf("underline") != -1) {
michael@0 2521 if (element.style.textDecoration == "underline") {
michael@0 2522 element.style.textDecoration = "";
michael@0 2523 } else {
michael@0 2524 element.style.textDecoration = element.style.textDecoration.replace("underline", "");
michael@0 2525 }
michael@0 2526 if (element.getAttribute("style") == "") {
michael@0 2527 element.removeAttribute("style");
michael@0 2528 }
michael@0 2529 }
michael@0 2530
michael@0 2531 // "If the relevant CSS property for command is not null, unset the CSS
michael@0 2532 // property property of element."
michael@0 2533 if (commands[command].relevantCssProperty !== null) {
michael@0 2534 element.style[commands[command].relevantCssProperty] = '';
michael@0 2535 if (element.getAttribute("style") == "") {
michael@0 2536 element.removeAttribute("style");
michael@0 2537 }
michael@0 2538 }
michael@0 2539
michael@0 2540 // "If element is a font element:"
michael@0 2541 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
michael@0 2542 // "If command is "foreColor", unset element's color attribute, if set."
michael@0 2543 if (command == "forecolor") {
michael@0 2544 element.removeAttribute("color");
michael@0 2545 }
michael@0 2546
michael@0 2547 // "If command is "fontName", unset element's face attribute, if set."
michael@0 2548 if (command == "fontname") {
michael@0 2549 element.removeAttribute("face");
michael@0 2550 }
michael@0 2551
michael@0 2552 // "If command is "fontSize", unset element's size attribute, if set."
michael@0 2553 if (command == "fontsize") {
michael@0 2554 element.removeAttribute("size");
michael@0 2555 }
michael@0 2556 }
michael@0 2557
michael@0 2558 // "If element is an a element and command is "createLink" or "unlink",
michael@0 2559 // unset the href property of element."
michael@0 2560 if (isHtmlElement(element, "A")
michael@0 2561 && (command == "createlink" || command == "unlink")) {
michael@0 2562 element.removeAttribute("href");
michael@0 2563 }
michael@0 2564
michael@0 2565 // "If element's specified command value for command is null, return the
michael@0 2566 // empty list."
michael@0 2567 if (getSpecifiedCommandValue(element, command) === null) {
michael@0 2568 return [];
michael@0 2569 }
michael@0 2570
michael@0 2571 // "Set the tag name of element to "span", and return the one-node list
michael@0 2572 // consisting of the result."
michael@0 2573 return [setTagName(element, "span")];
michael@0 2574 }
michael@0 2575
michael@0 2576
michael@0 2577 //@}
michael@0 2578 ///// Pushing down values /////
michael@0 2579 //@{
michael@0 2580
michael@0 2581 function pushDownValues(node, command, newValue) {
michael@0 2582 // "If node's parent is not an Element, abort this algorithm."
michael@0 2583 if (!node.parentNode
michael@0 2584 || node.parentNode.nodeType != Node.ELEMENT_NODE) {
michael@0 2585 return;
michael@0 2586 }
michael@0 2587
michael@0 2588 // "If the effective command value of command is loosely equivalent to new
michael@0 2589 // value on node, abort this algorithm."
michael@0 2590 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
michael@0 2591 return;
michael@0 2592 }
michael@0 2593
michael@0 2594 // "Let current ancestor be node's parent."
michael@0 2595 var currentAncestor = node.parentNode;
michael@0 2596
michael@0 2597 // "Let ancestor list be a list of Nodes, initially empty."
michael@0 2598 var ancestorList = [];
michael@0 2599
michael@0 2600 // "While current ancestor is an editable Element and the effective command
michael@0 2601 // value of command is not loosely equivalent to new value on it, append
michael@0 2602 // current ancestor to ancestor list, then set current ancestor to its
michael@0 2603 // parent."
michael@0 2604 while (isEditable(currentAncestor)
michael@0 2605 && currentAncestor.nodeType == Node.ELEMENT_NODE
michael@0 2606 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
michael@0 2607 ancestorList.push(currentAncestor);
michael@0 2608 currentAncestor = currentAncestor.parentNode;
michael@0 2609 }
michael@0 2610
michael@0 2611 // "If ancestor list is empty, abort this algorithm."
michael@0 2612 if (!ancestorList.length) {
michael@0 2613 return;
michael@0 2614 }
michael@0 2615
michael@0 2616 // "Let propagated value be the specified command value of command on the
michael@0 2617 // last member of ancestor list."
michael@0 2618 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
michael@0 2619
michael@0 2620 // "If propagated value is null and is not equal to new value, abort this
michael@0 2621 // algorithm."
michael@0 2622 if (propagatedValue === null && propagatedValue != newValue) {
michael@0 2623 return;
michael@0 2624 }
michael@0 2625
michael@0 2626 // "If the effective command value for the parent of the last member of
michael@0 2627 // ancestor list is not loosely equivalent to new value, and new value is
michael@0 2628 // not null, abort this algorithm."
michael@0 2629 if (newValue !== null
michael@0 2630 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
michael@0 2631 return;
michael@0 2632 }
michael@0 2633
michael@0 2634 // "While ancestor list is not empty:"
michael@0 2635 while (ancestorList.length) {
michael@0 2636 // "Let current ancestor be the last member of ancestor list."
michael@0 2637 // "Remove the last member from ancestor list."
michael@0 2638 var currentAncestor = ancestorList.pop();
michael@0 2639
michael@0 2640 // "If the specified command value of current ancestor for command is
michael@0 2641 // not null, set propagated value to that value."
michael@0 2642 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
michael@0 2643 propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
michael@0 2644 }
michael@0 2645
michael@0 2646 // "Let children be the children of current ancestor."
michael@0 2647 var children = Array.prototype.slice.call(currentAncestor.childNodes);
michael@0 2648
michael@0 2649 // "If the specified command value of current ancestor for command is
michael@0 2650 // not null, clear the value of current ancestor."
michael@0 2651 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
michael@0 2652 clearValue(currentAncestor, command);
michael@0 2653 }
michael@0 2654
michael@0 2655 // "For every child in children:"
michael@0 2656 for (var i = 0; i < children.length; i++) {
michael@0 2657 var child = children[i];
michael@0 2658
michael@0 2659 // "If child is node, continue with the next child."
michael@0 2660 if (child == node) {
michael@0 2661 continue;
michael@0 2662 }
michael@0 2663
michael@0 2664 // "If child is an Element whose specified command value for
michael@0 2665 // command is neither null nor equivalent to propagated value,
michael@0 2666 // continue with the next child."
michael@0 2667 if (child.nodeType == Node.ELEMENT_NODE
michael@0 2668 && getSpecifiedCommandValue(child, command) !== null
michael@0 2669 && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
michael@0 2670 continue;
michael@0 2671 }
michael@0 2672
michael@0 2673 // "If child is the last member of ancestor list, continue with the
michael@0 2674 // next child."
michael@0 2675 if (child == ancestorList[ancestorList.length - 1]) {
michael@0 2676 continue;
michael@0 2677 }
michael@0 2678
michael@0 2679 // "Force the value of child, with command as in this algorithm
michael@0 2680 // and new value equal to propagated value."
michael@0 2681 forceValue(child, command, propagatedValue);
michael@0 2682 }
michael@0 2683 }
michael@0 2684 }
michael@0 2685
michael@0 2686
michael@0 2687 //@}
michael@0 2688 ///// Forcing the value of a node /////
michael@0 2689 //@{
michael@0 2690
michael@0 2691 function forceValue(node, command, newValue) {
michael@0 2692 // "If node's parent is null, abort this algorithm."
michael@0 2693 if (!node.parentNode) {
michael@0 2694 return;
michael@0 2695 }
michael@0 2696
michael@0 2697 // "If new value is null, abort this algorithm."
michael@0 2698 if (newValue === null) {
michael@0 2699 return;
michael@0 2700 }
michael@0 2701
michael@0 2702 // "If node is an allowed child of "span":"
michael@0 2703 if (isAllowedChild(node, "span")) {
michael@0 2704 // "Reorder modifiable descendants of node's previousSibling."
michael@0 2705 reorderModifiableDescendants(node.previousSibling, command, newValue);
michael@0 2706
michael@0 2707 // "Reorder modifiable descendants of node's nextSibling."
michael@0 2708 reorderModifiableDescendants(node.nextSibling, command, newValue);
michael@0 2709
michael@0 2710 // "Wrap the one-node list consisting of node, with sibling criteria
michael@0 2711 // returning true for a simple modifiable element whose specified
michael@0 2712 // command value is equivalent to new value and whose effective command
michael@0 2713 // value is loosely equivalent to new value and false otherwise, and
michael@0 2714 // with new parent instructions returning null."
michael@0 2715 wrap([node],
michael@0 2716 function(node) {
michael@0 2717 return isSimpleModifiableElement(node)
michael@0 2718 && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
michael@0 2719 && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
michael@0 2720 },
michael@0 2721 function() { return null }
michael@0 2722 );
michael@0 2723 }
michael@0 2724
michael@0 2725 // "If node is invisible, abort this algorithm."
michael@0 2726 if (isInvisible(node)) {
michael@0 2727 return;
michael@0 2728 }
michael@0 2729
michael@0 2730 // "If the effective command value of command is loosely equivalent to new
michael@0 2731 // value on node, abort this algorithm."
michael@0 2732 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
michael@0 2733 return;
michael@0 2734 }
michael@0 2735
michael@0 2736 // "If node is not an allowed child of "span":"
michael@0 2737 if (!isAllowedChild(node, "span")) {
michael@0 2738 // "Let children be all children of node, omitting any that are
michael@0 2739 // Elements whose specified command value for command is neither null
michael@0 2740 // nor equivalent to new value."
michael@0 2741 var children = [];
michael@0 2742 for (var i = 0; i < node.childNodes.length; i++) {
michael@0 2743 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
michael@0 2744 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
michael@0 2745
michael@0 2746 if (specifiedValue !== null
michael@0 2747 && !areEquivalentValues(command, newValue, specifiedValue)) {
michael@0 2748 continue;
michael@0 2749 }
michael@0 2750 }
michael@0 2751 children.push(node.childNodes[i]);
michael@0 2752 }
michael@0 2753
michael@0 2754 // "Force the value of each Node in children, with command and new
michael@0 2755 // value as in this invocation of the algorithm."
michael@0 2756 for (var i = 0; i < children.length; i++) {
michael@0 2757 forceValue(children[i], command, newValue);
michael@0 2758 }
michael@0 2759
michael@0 2760 // "Abort this algorithm."
michael@0 2761 return;
michael@0 2762 }
michael@0 2763
michael@0 2764 // "If the effective command value of command is loosely equivalent to new
michael@0 2765 // value on node, abort this algorithm."
michael@0 2766 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
michael@0 2767 return;
michael@0 2768 }
michael@0 2769
michael@0 2770 // "Let new parent be null."
michael@0 2771 var newParent = null;
michael@0 2772
michael@0 2773 // "If the CSS styling flag is false:"
michael@0 2774 if (!cssStylingFlag) {
michael@0 2775 // "If command is "bold" and new value is "bold", let new parent be the
michael@0 2776 // result of calling createElement("b") on the ownerDocument of node."
michael@0 2777 if (command == "bold" && (newValue == "bold" || newValue == "700")) {
michael@0 2778 newParent = node.ownerDocument.createElement("b");
michael@0 2779 }
michael@0 2780
michael@0 2781 // "If command is "italic" and new value is "italic", let new parent be
michael@0 2782 // the result of calling createElement("i") on the ownerDocument of
michael@0 2783 // node."
michael@0 2784 if (command == "italic" && newValue == "italic") {
michael@0 2785 newParent = node.ownerDocument.createElement("i");
michael@0 2786 }
michael@0 2787
michael@0 2788 // "If command is "strikethrough" and new value is "line-through", let
michael@0 2789 // new parent be the result of calling createElement("s") on the
michael@0 2790 // ownerDocument of node."
michael@0 2791 if (command == "strikethrough" && newValue == "line-through") {
michael@0 2792 newParent = node.ownerDocument.createElement("s");
michael@0 2793 }
michael@0 2794
michael@0 2795 // "If command is "underline" and new value is "underline", let new
michael@0 2796 // parent be the result of calling createElement("u") on the
michael@0 2797 // ownerDocument of node."
michael@0 2798 if (command == "underline" && newValue == "underline") {
michael@0 2799 newParent = node.ownerDocument.createElement("u");
michael@0 2800 }
michael@0 2801
michael@0 2802 // "If command is "foreColor", and new value is fully opaque with red,
michael@0 2803 // green, and blue components in the range 0 to 255:"
michael@0 2804 if (command == "forecolor" && parseSimpleColor(newValue)) {
michael@0 2805 // "Let new parent be the result of calling createElement("font")
michael@0 2806 // on the ownerDocument of node."
michael@0 2807 newParent = node.ownerDocument.createElement("font");
michael@0 2808
michael@0 2809 // "Set the color attribute of new parent to the result of applying
michael@0 2810 // the rules for serializing simple color values to new value
michael@0 2811 // (interpreted as a simple color)."
michael@0 2812 newParent.setAttribute("color", parseSimpleColor(newValue));
michael@0 2813 }
michael@0 2814
michael@0 2815 // "If command is "fontName", let new parent be the result of calling
michael@0 2816 // createElement("font") on the ownerDocument of node, then set the
michael@0 2817 // face attribute of new parent to new value."
michael@0 2818 if (command == "fontname") {
michael@0 2819 newParent = node.ownerDocument.createElement("font");
michael@0 2820 newParent.face = newValue;
michael@0 2821 }
michael@0 2822 }
michael@0 2823
michael@0 2824 // "If command is "createLink" or "unlink":"
michael@0 2825 if (command == "createlink" || command == "unlink") {
michael@0 2826 // "Let new parent be the result of calling createElement("a") on the
michael@0 2827 // ownerDocument of node."
michael@0 2828 newParent = node.ownerDocument.createElement("a");
michael@0 2829
michael@0 2830 // "Set the href attribute of new parent to new value."
michael@0 2831 newParent.setAttribute("href", newValue);
michael@0 2832
michael@0 2833 // "Let ancestor be node's parent."
michael@0 2834 var ancestor = node.parentNode;
michael@0 2835
michael@0 2836 // "While ancestor is not null:"
michael@0 2837 while (ancestor) {
michael@0 2838 // "If ancestor is an a, set the tag name of ancestor to "span",
michael@0 2839 // and let ancestor be the result."
michael@0 2840 if (isHtmlElement(ancestor, "A")) {
michael@0 2841 ancestor = setTagName(ancestor, "span");
michael@0 2842 }
michael@0 2843
michael@0 2844 // "Set ancestor to its parent."
michael@0 2845 ancestor = ancestor.parentNode;
michael@0 2846 }
michael@0 2847 }
michael@0 2848
michael@0 2849 // "If command is "fontSize"; and new value is one of "x-small", "small",
michael@0 2850 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
michael@0 2851 // CSS styling flag is false, or new value is "xxx-large": let new parent
michael@0 2852 // be the result of calling createElement("font") on the ownerDocument of
michael@0 2853 // node, then set the size attribute of new parent to the number from the
michael@0 2854 // following table based on new value: [table omitted]"
michael@0 2855 if (command == "fontsize"
michael@0 2856 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
michael@0 2857 && (!cssStylingFlag || newValue == "xxx-large")) {
michael@0 2858 newParent = node.ownerDocument.createElement("font");
michael@0 2859 newParent.size = cssSizeToLegacy(newValue);
michael@0 2860 }
michael@0 2861
michael@0 2862 // "If command is "subscript" or "superscript" and new value is
michael@0 2863 // "subscript", let new parent be the result of calling
michael@0 2864 // createElement("sub") on the ownerDocument of node."
michael@0 2865 if ((command == "subscript" || command == "superscript")
michael@0 2866 && newValue == "subscript") {
michael@0 2867 newParent = node.ownerDocument.createElement("sub");
michael@0 2868 }
michael@0 2869
michael@0 2870 // "If command is "subscript" or "superscript" and new value is
michael@0 2871 // "superscript", let new parent be the result of calling
michael@0 2872 // createElement("sup") on the ownerDocument of node."
michael@0 2873 if ((command == "subscript" || command == "superscript")
michael@0 2874 && newValue == "superscript") {
michael@0 2875 newParent = node.ownerDocument.createElement("sup");
michael@0 2876 }
michael@0 2877
michael@0 2878 // "If new parent is null, let new parent be the result of calling
michael@0 2879 // createElement("span") on the ownerDocument of node."
michael@0 2880 if (!newParent) {
michael@0 2881 newParent = node.ownerDocument.createElement("span");
michael@0 2882 }
michael@0 2883
michael@0 2884 // "Insert new parent in node's parent before node."
michael@0 2885 node.parentNode.insertBefore(newParent, node);
michael@0 2886
michael@0 2887 // "If the effective command value of command for new parent is not loosely
michael@0 2888 // equivalent to new value, and the relevant CSS property for command is
michael@0 2889 // not null, set that CSS property of new parent to new value (if the new
michael@0 2890 // value would be valid)."
michael@0 2891 var property = commands[command].relevantCssProperty;
michael@0 2892 if (property !== null
michael@0 2893 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
michael@0 2894 newParent.style[property] = newValue;
michael@0 2895 }
michael@0 2896
michael@0 2897 // "If command is "strikethrough", and new value is "line-through", and the
michael@0 2898 // effective command value of "strikethrough" for new parent is not
michael@0 2899 // "line-through", set the "text-decoration" property of new parent to
michael@0 2900 // "line-through"."
michael@0 2901 if (command == "strikethrough"
michael@0 2902 && newValue == "line-through"
michael@0 2903 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
michael@0 2904 newParent.style.textDecoration = "line-through";
michael@0 2905 }
michael@0 2906
michael@0 2907 // "If command is "underline", and new value is "underline", and the
michael@0 2908 // effective command value of "underline" for new parent is not
michael@0 2909 // "underline", set the "text-decoration" property of new parent to
michael@0 2910 // "underline"."
michael@0 2911 if (command == "underline"
michael@0 2912 && newValue == "underline"
michael@0 2913 && getEffectiveCommandValue(newParent, "underline") != "underline") {
michael@0 2914 newParent.style.textDecoration = "underline";
michael@0 2915 }
michael@0 2916
michael@0 2917 // "Append node to new parent as its last child, preserving ranges."
michael@0 2918 movePreservingRanges(node, newParent, newParent.childNodes.length);
michael@0 2919
michael@0 2920 // "If node is an Element and the effective command value of command for
michael@0 2921 // node is not loosely equivalent to new value:"
michael@0 2922 if (node.nodeType == Node.ELEMENT_NODE
michael@0 2923 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
michael@0 2924 // "Insert node into the parent of new parent before new parent,
michael@0 2925 // preserving ranges."
michael@0 2926 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
michael@0 2927
michael@0 2928 // "Remove new parent from its parent."
michael@0 2929 newParent.parentNode.removeChild(newParent);
michael@0 2930
michael@0 2931 // "Let children be all children of node, omitting any that are
michael@0 2932 // Elements whose specified command value for command is neither null
michael@0 2933 // nor equivalent to new value."
michael@0 2934 var children = [];
michael@0 2935 for (var i = 0; i < node.childNodes.length; i++) {
michael@0 2936 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
michael@0 2937 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
michael@0 2938
michael@0 2939 if (specifiedValue !== null
michael@0 2940 && !areEquivalentValues(command, newValue, specifiedValue)) {
michael@0 2941 continue;
michael@0 2942 }
michael@0 2943 }
michael@0 2944 children.push(node.childNodes[i]);
michael@0 2945 }
michael@0 2946
michael@0 2947 // "Force the value of each Node in children, with command and new
michael@0 2948 // value as in this invocation of the algorithm."
michael@0 2949 for (var i = 0; i < children.length; i++) {
michael@0 2950 forceValue(children[i], command, newValue);
michael@0 2951 }
michael@0 2952 }
michael@0 2953 }
michael@0 2954
michael@0 2955
michael@0 2956 //@}
michael@0 2957 ///// Setting the selection's value /////
michael@0 2958 //@{
michael@0 2959
michael@0 2960 function setSelectionValue(command, newValue) {
michael@0 2961 // "If there is no formattable node effectively contained in the active
michael@0 2962 // range:"
michael@0 2963 if (!getAllEffectivelyContainedNodes(getActiveRange())
michael@0 2964 .some(isFormattableNode)) {
michael@0 2965 // "If command has inline command activated values, set the state
michael@0 2966 // override to true if new value is among them and false if it's not."
michael@0 2967 if ("inlineCommandActivatedValues" in commands[command]) {
michael@0 2968 setStateOverride(command, commands[command].inlineCommandActivatedValues
michael@0 2969 .indexOf(newValue) != -1);
michael@0 2970 }
michael@0 2971
michael@0 2972 // "If command is "subscript", unset the state override for
michael@0 2973 // "superscript"."
michael@0 2974 if (command == "subscript") {
michael@0 2975 unsetStateOverride("superscript");
michael@0 2976 }
michael@0 2977
michael@0 2978 // "If command is "superscript", unset the state override for
michael@0 2979 // "subscript"."
michael@0 2980 if (command == "superscript") {
michael@0 2981 unsetStateOverride("subscript");
michael@0 2982 }
michael@0 2983
michael@0 2984 // "If new value is null, unset the value override (if any)."
michael@0 2985 if (newValue === null) {
michael@0 2986 unsetValueOverride(command);
michael@0 2987
michael@0 2988 // "Otherwise, if command is "createLink" or it has a value specified,
michael@0 2989 // set the value override to new value."
michael@0 2990 } else if (command == "createlink" || "value" in commands[command]) {
michael@0 2991 setValueOverride(command, newValue);
michael@0 2992 }
michael@0 2993
michael@0 2994 // "Abort these steps."
michael@0 2995 return;
michael@0 2996 }
michael@0 2997
michael@0 2998 // "If the active range's start node is an editable Text node, and its
michael@0 2999 // start offset is neither zero nor its start node's length, call
michael@0 3000 // splitText() on the active range's start node, with argument equal to the
michael@0 3001 // active range's start offset. Then set the active range's start node to
michael@0 3002 // the result, and its start offset to zero."
michael@0 3003 if (isEditable(getActiveRange().startContainer)
michael@0 3004 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 3005 && getActiveRange().startOffset != 0
michael@0 3006 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
michael@0 3007 // Account for browsers not following range mutation rules
michael@0 3008 var newActiveRange = document.createRange();
michael@0 3009 var newNode;
michael@0 3010 if (getActiveRange().startContainer == getActiveRange().endContainer) {
michael@0 3011 var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset;
michael@0 3012 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
michael@0 3013 newActiveRange.setEnd(newNode, newEndOffset);
michael@0 3014 getActiveRange().setEnd(newNode, newEndOffset);
michael@0 3015 } else {
michael@0 3016 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
michael@0 3017 }
michael@0 3018 newActiveRange.setStart(newNode, 0);
michael@0 3019 getSelection().removeAllRanges();
michael@0 3020 getSelection().addRange(newActiveRange);
michael@0 3021
michael@0 3022 getActiveRange().setStart(newNode, 0);
michael@0 3023 }
michael@0 3024
michael@0 3025 // "If the active range's end node is an editable Text node, and its end
michael@0 3026 // offset is neither zero nor its end node's length, call splitText() on
michael@0 3027 // the active range's end node, with argument equal to the active range's
michael@0 3028 // end offset."
michael@0 3029 if (isEditable(getActiveRange().endContainer)
michael@0 3030 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
michael@0 3031 && getActiveRange().endOffset != 0
michael@0 3032 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
michael@0 3033 // IE seems to mutate the range incorrectly here, so we need correction
michael@0 3034 // here as well. The active range will be temporarily in orphaned
michael@0 3035 // nodes, so calling getActiveRange() after splitText() but before
michael@0 3036 // fixing the range will throw an exception.
michael@0 3037 var activeRange = getActiveRange();
michael@0 3038 var newStart = [activeRange.startContainer, activeRange.startOffset];
michael@0 3039 var newEnd = [activeRange.endContainer, activeRange.endOffset];
michael@0 3040 activeRange.endContainer.splitText(activeRange.endOffset);
michael@0 3041 activeRange.setStart(newStart[0], newStart[1]);
michael@0 3042 activeRange.setEnd(newEnd[0], newEnd[1]);
michael@0 3043
michael@0 3044 getSelection().removeAllRanges();
michael@0 3045 getSelection().addRange(activeRange);
michael@0 3046 }
michael@0 3047
michael@0 3048 // "Let element list be all editable Elements effectively contained in the
michael@0 3049 // active range.
michael@0 3050 //
michael@0 3051 // "For each element in element list, clear the value of element."
michael@0 3052 getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
michael@0 3053 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
michael@0 3054 }).forEach(function(element) {
michael@0 3055 clearValue(element, command);
michael@0 3056 });
michael@0 3057
michael@0 3058 // "Let node list be all editable nodes effectively contained in the active
michael@0 3059 // range.
michael@0 3060 //
michael@0 3061 // "For each node in node list:"
michael@0 3062 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
michael@0 3063 // "Push down values on node."
michael@0 3064 pushDownValues(node, command, newValue);
michael@0 3065
michael@0 3066 // "If node is an allowed child of span, force the value of node."
michael@0 3067 if (isAllowedChild(node, "span")) {
michael@0 3068 forceValue(node, command, newValue);
michael@0 3069 }
michael@0 3070 });
michael@0 3071 }
michael@0 3072
michael@0 3073
michael@0 3074 //@}
michael@0 3075 ///// The backColor command /////
michael@0 3076 //@{
michael@0 3077 commands.backcolor = {
michael@0 3078 // Copy-pasted, same as hiliteColor
michael@0 3079 action: function(value) {
michael@0 3080 // Action is further copy-pasted, same as foreColor
michael@0 3081
michael@0 3082 // "If value is not a valid CSS color, prepend "#" to it."
michael@0 3083 //
michael@0 3084 // "If value is still not a valid CSS color, or if it is currentColor,
michael@0 3085 // return false."
michael@0 3086 //
michael@0 3087 // Cheap hack for testing, no attempt to be comprehensive.
michael@0 3088 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
michael@0 3089 value = "#" + value;
michael@0 3090 }
michael@0 3091 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
michael@0 3092 && !parseSimpleColor(value)
michael@0 3093 && value.toLowerCase() != "transparent") {
michael@0 3094 return false;
michael@0 3095 }
michael@0 3096
michael@0 3097 // "Set the selection's value to value."
michael@0 3098 setSelectionValue("backcolor", value);
michael@0 3099
michael@0 3100 // "Return true."
michael@0 3101 return true;
michael@0 3102 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
michael@0 3103 equivalentValues: function(val1, val2) {
michael@0 3104 // "Either both strings are valid CSS colors and have the same red,
michael@0 3105 // green, blue, and alpha components, or neither string is a valid CSS
michael@0 3106 // color."
michael@0 3107 return normalizeColor(val1) === normalizeColor(val2);
michael@0 3108 },
michael@0 3109 };
michael@0 3110
michael@0 3111 //@}
michael@0 3112 ///// The bold command /////
michael@0 3113 //@{
michael@0 3114 commands.bold = {
michael@0 3115 action: function() {
michael@0 3116 // "If queryCommandState("bold") returns true, set the selection's
michael@0 3117 // value to "normal". Otherwise set the selection's value to "bold".
michael@0 3118 // Either way, return true."
michael@0 3119 if (myQueryCommandState("bold")) {
michael@0 3120 setSelectionValue("bold", "normal");
michael@0 3121 } else {
michael@0 3122 setSelectionValue("bold", "bold");
michael@0 3123 }
michael@0 3124 return true;
michael@0 3125 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
michael@0 3126 relevantCssProperty: "fontWeight",
michael@0 3127 equivalentValues: function(val1, val2) {
michael@0 3128 // "Either the two strings are equal, or one is "bold" and the other is
michael@0 3129 // "700", or one is "normal" and the other is "400"."
michael@0 3130 return val1 == val2
michael@0 3131 || (val1 == "bold" && val2 == "700")
michael@0 3132 || (val1 == "700" && val2 == "bold")
michael@0 3133 || (val1 == "normal" && val2 == "400")
michael@0 3134 || (val1 == "400" && val2 == "normal");
michael@0 3135 },
michael@0 3136 };
michael@0 3137
michael@0 3138 //@}
michael@0 3139 ///// The createLink command /////
michael@0 3140 //@{
michael@0 3141 commands.createlink = {
michael@0 3142 action: function(value) {
michael@0 3143 // "If value is the empty string, return false."
michael@0 3144 if (value === "") {
michael@0 3145 return false;
michael@0 3146 }
michael@0 3147
michael@0 3148 // "For each editable a element that has an href attribute and is an
michael@0 3149 // ancestor of some node effectively contained in the active range, set
michael@0 3150 // that a element's href attribute to value."
michael@0 3151 //
michael@0 3152 // TODO: We don't actually do this in tree order, not that it matters
michael@0 3153 // unless you're spying with mutation events.
michael@0 3154 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
michael@0 3155 getAncestors(node).forEach(function(ancestor) {
michael@0 3156 if (isEditable(ancestor)
michael@0 3157 && isHtmlElement(ancestor, "a")
michael@0 3158 && ancestor.hasAttribute("href")) {
michael@0 3159 ancestor.setAttribute("href", value);
michael@0 3160 }
michael@0 3161 });
michael@0 3162 });
michael@0 3163
michael@0 3164 // "Set the selection's value to value."
michael@0 3165 setSelectionValue("createlink", value);
michael@0 3166
michael@0 3167 // "Return true."
michael@0 3168 return true;
michael@0 3169 }
michael@0 3170 };
michael@0 3171
michael@0 3172 //@}
michael@0 3173 ///// The fontName command /////
michael@0 3174 //@{
michael@0 3175 commands.fontname = {
michael@0 3176 action: function(value) {
michael@0 3177 // "Set the selection's value to value, then return true."
michael@0 3178 setSelectionValue("fontname", value);
michael@0 3179 return true;
michael@0 3180 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
michael@0 3181 };
michael@0 3182
michael@0 3183 //@}
michael@0 3184 ///// The fontSize command /////
michael@0 3185 //@{
michael@0 3186
michael@0 3187 // Helper function for fontSize's action plus queryOutputHelper. It's just the
michael@0 3188 // middle of fontSize's action, ripped out into its own function. Returns null
michael@0 3189 // if the size is invalid.
michael@0 3190 function normalizeFontSize(value) {
michael@0 3191 // "Strip leading and trailing whitespace from value."
michael@0 3192 //
michael@0 3193 // Cheap hack, not following the actual algorithm.
michael@0 3194 value = value.trim();
michael@0 3195
michael@0 3196 // "If value is not a valid floating point number, and would not be a valid
michael@0 3197 // floating point number if a single leading "+" character were stripped,
michael@0 3198 // return false."
michael@0 3199 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
michael@0 3200 return null;
michael@0 3201 }
michael@0 3202
michael@0 3203 var mode;
michael@0 3204
michael@0 3205 // "If the first character of value is "+", delete the character and let
michael@0 3206 // mode be "relative-plus"."
michael@0 3207 if (value[0] == "+") {
michael@0 3208 value = value.slice(1);
michael@0 3209 mode = "relative-plus";
michael@0 3210 // "Otherwise, if the first character of value is "-", delete the character
michael@0 3211 // and let mode be "relative-minus"."
michael@0 3212 } else if (value[0] == "-") {
michael@0 3213 value = value.slice(1);
michael@0 3214 mode = "relative-minus";
michael@0 3215 // "Otherwise, let mode be "absolute"."
michael@0 3216 } else {
michael@0 3217 mode = "absolute";
michael@0 3218 }
michael@0 3219
michael@0 3220 // "Apply the rules for parsing non-negative integers to value, and let
michael@0 3221 // number be the result."
michael@0 3222 //
michael@0 3223 // Another cheap hack.
michael@0 3224 var num = parseInt(value);
michael@0 3225
michael@0 3226 // "If mode is "relative-plus", add three to number."
michael@0 3227 if (mode == "relative-plus") {
michael@0 3228 num += 3;
michael@0 3229 }
michael@0 3230
michael@0 3231 // "If mode is "relative-minus", negate number, then add three to it."
michael@0 3232 if (mode == "relative-minus") {
michael@0 3233 num = 3 - num;
michael@0 3234 }
michael@0 3235
michael@0 3236 // "If number is less than one, let number equal 1."
michael@0 3237 if (num < 1) {
michael@0 3238 num = 1;
michael@0 3239 }
michael@0 3240
michael@0 3241 // "If number is greater than seven, let number equal 7."
michael@0 3242 if (num > 7) {
michael@0 3243 num = 7;
michael@0 3244 }
michael@0 3245
michael@0 3246 // "Set value to the string here corresponding to number:" [table omitted]
michael@0 3247 value = {
michael@0 3248 1: "x-small",
michael@0 3249 2: "small",
michael@0 3250 3: "medium",
michael@0 3251 4: "large",
michael@0 3252 5: "x-large",
michael@0 3253 6: "xx-large",
michael@0 3254 7: "xxx-large"
michael@0 3255 }[num];
michael@0 3256
michael@0 3257 return value;
michael@0 3258 }
michael@0 3259
michael@0 3260 commands.fontsize = {
michael@0 3261 action: function(value) {
michael@0 3262 value = normalizeFontSize(value);
michael@0 3263 if (value === null) {
michael@0 3264 return false;
michael@0 3265 }
michael@0 3266
michael@0 3267 // "Set the selection's value to value."
michael@0 3268 setSelectionValue("fontsize", value);
michael@0 3269
michael@0 3270 // "Return true."
michael@0 3271 return true;
michael@0 3272 }, indeterm: function() {
michael@0 3273 // "True if among formattable nodes that are effectively contained in
michael@0 3274 // the active range, there are two that have distinct effective command
michael@0 3275 // values. Otherwise false."
michael@0 3276 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
michael@0 3277 .map(function(node) {
michael@0 3278 return getEffectiveCommandValue(node, "fontsize");
michael@0 3279 }).filter(function(value, i, arr) {
michael@0 3280 return arr.slice(0, i).indexOf(value) == -1;
michael@0 3281 }).length >= 2;
michael@0 3282 }, value: function() {
michael@0 3283 // "If the active range is null, return the empty string."
michael@0 3284 if (!getActiveRange()) {
michael@0 3285 return "";
michael@0 3286 }
michael@0 3287
michael@0 3288 // "Let pixel size be the effective command value of the first
michael@0 3289 // formattable node that is effectively contained in the active range,
michael@0 3290 // or if there is no such node, the effective command value of the
michael@0 3291 // active range's start node, in either case interpreted as a number of
michael@0 3292 // pixels."
michael@0 3293 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
michael@0 3294 if (node === undefined) {
michael@0 3295 node = getActiveRange().startContainer;
michael@0 3296 }
michael@0 3297 var pixelSize = getEffectiveCommandValue(node, "fontsize");
michael@0 3298
michael@0 3299 // "Return the legacy font size for pixel size."
michael@0 3300 return getLegacyFontSize(pixelSize);
michael@0 3301 }, relevantCssProperty: "fontSize"
michael@0 3302 };
michael@0 3303
michael@0 3304 function getLegacyFontSize(size) {
michael@0 3305 if (getLegacyFontSize.resultCache === undefined) {
michael@0 3306 getLegacyFontSize.resultCache = {};
michael@0 3307 }
michael@0 3308
michael@0 3309 if (getLegacyFontSize.resultCache[size] !== undefined) {
michael@0 3310 return getLegacyFontSize.resultCache[size];
michael@0 3311 }
michael@0 3312
michael@0 3313 // For convenience in other places in my code, I handle all sizes, not just
michael@0 3314 // pixel sizes as the spec says. This means pixel sizes have to be passed
michael@0 3315 // in suffixed with "px", not as plain numbers.
michael@0 3316 if (normalizeFontSize(size) !== null) {
michael@0 3317 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size));
michael@0 3318 }
michael@0 3319
michael@0 3320 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1
michael@0 3321 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
michael@0 3322 // There is no sensible legacy size for things like "2em".
michael@0 3323 return getLegacyFontSize.resultCache[size] = null;
michael@0 3324 }
michael@0 3325
michael@0 3326 var font = document.createElement("font");
michael@0 3327 document.body.appendChild(font);
michael@0 3328 if (size == "xxx-large") {
michael@0 3329 font.size = 7;
michael@0 3330 } else {
michael@0 3331 font.style.fontSize = size;
michael@0 3332 }
michael@0 3333 var pixelSize = parseInt(getComputedStyle(font).fontSize);
michael@0 3334 document.body.removeChild(font);
michael@0 3335
michael@0 3336 // "Let returned size be 1."
michael@0 3337 var returnedSize = 1;
michael@0 3338
michael@0 3339 // "While returned size is less than 7:"
michael@0 3340 while (returnedSize < 7) {
michael@0 3341 // "Let lower bound be the resolved value of "font-size" in pixels
michael@0 3342 // of a font element whose size attribute is set to returned size."
michael@0 3343 var font = document.createElement("font");
michael@0 3344 font.size = returnedSize;
michael@0 3345 document.body.appendChild(font);
michael@0 3346 var lowerBound = parseInt(getComputedStyle(font).fontSize);
michael@0 3347
michael@0 3348 // "Let upper bound be the resolved value of "font-size" in pixels
michael@0 3349 // of a font element whose size attribute is set to one plus
michael@0 3350 // returned size."
michael@0 3351 font.size = 1 + returnedSize;
michael@0 3352 var upperBound = parseInt(getComputedStyle(font).fontSize);
michael@0 3353 document.body.removeChild(font);
michael@0 3354
michael@0 3355 // "Let average be the average of upper bound and lower bound."
michael@0 3356 var average = (upperBound + lowerBound)/2;
michael@0 3357
michael@0 3358 // "If pixel size is less than average, return the one-element
michael@0 3359 // string consisting of the digit returned size."
michael@0 3360 if (pixelSize < average) {
michael@0 3361 return getLegacyFontSize.resultCache[size] = String(returnedSize);
michael@0 3362 }
michael@0 3363
michael@0 3364 // "Add one to returned size."
michael@0 3365 returnedSize++;
michael@0 3366 }
michael@0 3367
michael@0 3368 // "Return "7"."
michael@0 3369 return getLegacyFontSize.resultCache[size] = "7";
michael@0 3370 }
michael@0 3371
michael@0 3372 //@}
michael@0 3373 ///// The foreColor command /////
michael@0 3374 //@{
michael@0 3375 commands.forecolor = {
michael@0 3376 action: function(value) {
michael@0 3377 // Copy-pasted, same as backColor and hiliteColor
michael@0 3378
michael@0 3379 // "If value is not a valid CSS color, prepend "#" to it."
michael@0 3380 //
michael@0 3381 // "If value is still not a valid CSS color, or if it is currentColor,
michael@0 3382 // return false."
michael@0 3383 //
michael@0 3384 // Cheap hack for testing, no attempt to be comprehensive.
michael@0 3385 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
michael@0 3386 value = "#" + value;
michael@0 3387 }
michael@0 3388 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
michael@0 3389 && !parseSimpleColor(value)
michael@0 3390 && value.toLowerCase() != "transparent") {
michael@0 3391 return false;
michael@0 3392 }
michael@0 3393
michael@0 3394 // "Set the selection's value to value."
michael@0 3395 setSelectionValue("forecolor", value);
michael@0 3396
michael@0 3397 // "Return true."
michael@0 3398 return true;
michael@0 3399 }, standardInlineValueCommand: true, relevantCssProperty: "color",
michael@0 3400 equivalentValues: function(val1, val2) {
michael@0 3401 // "Either both strings are valid CSS colors and have the same red,
michael@0 3402 // green, blue, and alpha components, or neither string is a valid CSS
michael@0 3403 // color."
michael@0 3404 return normalizeColor(val1) === normalizeColor(val2);
michael@0 3405 },
michael@0 3406 };
michael@0 3407
michael@0 3408 //@}
michael@0 3409 ///// The hiliteColor command /////
michael@0 3410 //@{
michael@0 3411 commands.hilitecolor = {
michael@0 3412 // Copy-pasted, same as backColor
michael@0 3413 action: function(value) {
michael@0 3414 // Action is further copy-pasted, same as foreColor
michael@0 3415
michael@0 3416 // "If value is not a valid CSS color, prepend "#" to it."
michael@0 3417 //
michael@0 3418 // "If value is still not a valid CSS color, or if it is currentColor,
michael@0 3419 // return false."
michael@0 3420 //
michael@0 3421 // Cheap hack for testing, no attempt to be comprehensive.
michael@0 3422 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
michael@0 3423 value = "#" + value;
michael@0 3424 }
michael@0 3425 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
michael@0 3426 && !parseSimpleColor(value)
michael@0 3427 && value.toLowerCase() != "transparent") {
michael@0 3428 return false;
michael@0 3429 }
michael@0 3430
michael@0 3431 // "Set the selection's value to value."
michael@0 3432 setSelectionValue("hilitecolor", value);
michael@0 3433
michael@0 3434 // "Return true."
michael@0 3435 return true;
michael@0 3436 }, indeterm: function() {
michael@0 3437 // "True if among editable Text nodes that are effectively contained in
michael@0 3438 // the active range, there are two that have distinct effective command
michael@0 3439 // values. Otherwise false."
michael@0 3440 return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
michael@0 3441 return isEditable(node) && node.nodeType == Node.TEXT_NODE;
michael@0 3442 }).map(function(node) {
michael@0 3443 return getEffectiveCommandValue(node, "hilitecolor");
michael@0 3444 }).filter(function(value, i, arr) {
michael@0 3445 return arr.slice(0, i).indexOf(value) == -1;
michael@0 3446 }).length >= 2;
michael@0 3447 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
michael@0 3448 equivalentValues: function(val1, val2) {
michael@0 3449 // "Either both strings are valid CSS colors and have the same red,
michael@0 3450 // green, blue, and alpha components, or neither string is a valid CSS
michael@0 3451 // color."
michael@0 3452 return normalizeColor(val1) === normalizeColor(val2);
michael@0 3453 },
michael@0 3454 };
michael@0 3455
michael@0 3456 //@}
michael@0 3457 ///// The italic command /////
michael@0 3458 //@{
michael@0 3459 commands.italic = {
michael@0 3460 action: function() {
michael@0 3461 // "If queryCommandState("italic") returns true, set the selection's
michael@0 3462 // value to "normal". Otherwise set the selection's value to "italic".
michael@0 3463 // Either way, return true."
michael@0 3464 if (myQueryCommandState("italic")) {
michael@0 3465 setSelectionValue("italic", "normal");
michael@0 3466 } else {
michael@0 3467 setSelectionValue("italic", "italic");
michael@0 3468 }
michael@0 3469 return true;
michael@0 3470 }, inlineCommandActivatedValues: ["italic", "oblique"],
michael@0 3471 relevantCssProperty: "fontStyle"
michael@0 3472 };
michael@0 3473
michael@0 3474 //@}
michael@0 3475 ///// The removeFormat command /////
michael@0 3476 //@{
michael@0 3477 commands.removeformat = {
michael@0 3478 action: function() {
michael@0 3479 // "A removeFormat candidate is an editable HTML element with local
michael@0 3480 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
michael@0 3481 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
michael@0 3482 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
michael@0 3483 // "tt", "u", or "var"."
michael@0 3484 function isRemoveFormatCandidate(node) {
michael@0 3485 return isEditable(node)
michael@0 3486 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
michael@0 3487 "big", "blink", "cite", "code", "dfn", "em", "font", "i",
michael@0 3488 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
michael@0 3489 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
michael@0 3490 }
michael@0 3491
michael@0 3492 // "Let elements to remove be a list of every removeFormat candidate
michael@0 3493 // effectively contained in the active range."
michael@0 3494 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
michael@0 3495
michael@0 3496 // "For each element in elements to remove:"
michael@0 3497 elementsToRemove.forEach(function(element) {
michael@0 3498 // "While element has children, insert the first child of element
michael@0 3499 // into the parent of element immediately before element,
michael@0 3500 // preserving ranges."
michael@0 3501 while (element.hasChildNodes()) {
michael@0 3502 movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
michael@0 3503 }
michael@0 3504
michael@0 3505 // "Remove element from its parent."
michael@0 3506 element.parentNode.removeChild(element);
michael@0 3507 });
michael@0 3508
michael@0 3509 // "If the active range's start node is an editable Text node, and its
michael@0 3510 // start offset is neither zero nor its start node's length, call
michael@0 3511 // splitText() on the active range's start node, with argument equal to
michael@0 3512 // the active range's start offset. Then set the active range's start
michael@0 3513 // node to the result, and its start offset to zero."
michael@0 3514 if (isEditable(getActiveRange().startContainer)
michael@0 3515 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 3516 && getActiveRange().startOffset != 0
michael@0 3517 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
michael@0 3518 // Account for browsers not following range mutation rules
michael@0 3519 if (getActiveRange().startContainer == getActiveRange().endContainer) {
michael@0 3520 var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
michael@0 3521 var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
michael@0 3522 getActiveRange().setStart(newNode, 0);
michael@0 3523 getActiveRange().setEnd(newNode, newEnd);
michael@0 3524 } else {
michael@0 3525 getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
michael@0 3526 }
michael@0 3527 }
michael@0 3528
michael@0 3529 // "If the active range's end node is an editable Text node, and its
michael@0 3530 // end offset is neither zero nor its end node's length, call
michael@0 3531 // splitText() on the active range's end node, with argument equal to
michael@0 3532 // the active range's end offset."
michael@0 3533 if (isEditable(getActiveRange().endContainer)
michael@0 3534 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
michael@0 3535 && getActiveRange().endOffset != 0
michael@0 3536 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
michael@0 3537 // IE seems to mutate the range incorrectly here, so we need
michael@0 3538 // correction here as well. Have to be careful to set the range to
michael@0 3539 // something not including the text node so that getActiveRange()
michael@0 3540 // doesn't throw an exception due to a temporarily detached
michael@0 3541 // endpoint.
michael@0 3542 var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
michael@0 3543 var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
michael@0 3544 getActiveRange().setEnd(document.documentElement, 0);
michael@0 3545 newEnd[0].splitText(newEnd[1]);
michael@0 3546 getActiveRange().setStart(newStart[0], newStart[1]);
michael@0 3547 getActiveRange().setEnd(newEnd[0], newEnd[1]);
michael@0 3548 }
michael@0 3549
michael@0 3550 // "Let node list consist of all editable nodes effectively contained
michael@0 3551 // in the active range."
michael@0 3552 //
michael@0 3553 // "For each node in node list, while node's parent is a removeFormat
michael@0 3554 // candidate in the same editing host as node, split the parent of the
michael@0 3555 // one-node list consisting of node."
michael@0 3556 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
michael@0 3557 while (isRemoveFormatCandidate(node.parentNode)
michael@0 3558 && inSameEditingHost(node.parentNode, node)) {
michael@0 3559 splitParent([node]);
michael@0 3560 }
michael@0 3561 });
michael@0 3562
michael@0 3563 // "For each of the entries in the following list, in the given order,
michael@0 3564 // set the selection's value to null, with command as given."
michael@0 3565 [
michael@0 3566 "subscript",
michael@0 3567 "bold",
michael@0 3568 "fontname",
michael@0 3569 "fontsize",
michael@0 3570 "forecolor",
michael@0 3571 "hilitecolor",
michael@0 3572 "italic",
michael@0 3573 "strikethrough",
michael@0 3574 "underline",
michael@0 3575 ].forEach(function(command) {
michael@0 3576 setSelectionValue(command, null);
michael@0 3577 });
michael@0 3578
michael@0 3579 // "Return true."
michael@0 3580 return true;
michael@0 3581 }
michael@0 3582 };
michael@0 3583
michael@0 3584 //@}
michael@0 3585 ///// The strikethrough command /////
michael@0 3586 //@{
michael@0 3587 commands.strikethrough = {
michael@0 3588 action: function() {
michael@0 3589 // "If queryCommandState("strikethrough") returns true, set the
michael@0 3590 // selection's value to null. Otherwise set the selection's value to
michael@0 3591 // "line-through". Either way, return true."
michael@0 3592 if (myQueryCommandState("strikethrough")) {
michael@0 3593 setSelectionValue("strikethrough", null);
michael@0 3594 } else {
michael@0 3595 setSelectionValue("strikethrough", "line-through");
michael@0 3596 }
michael@0 3597 return true;
michael@0 3598 }, inlineCommandActivatedValues: ["line-through"]
michael@0 3599 };
michael@0 3600
michael@0 3601 //@}
michael@0 3602 ///// The subscript command /////
michael@0 3603 //@{
michael@0 3604 commands.subscript = {
michael@0 3605 action: function() {
michael@0 3606 // "Call queryCommandState("subscript"), and let state be the result."
michael@0 3607 var state = myQueryCommandState("subscript");
michael@0 3608
michael@0 3609 // "Set the selection's value to null."
michael@0 3610 setSelectionValue("subscript", null);
michael@0 3611
michael@0 3612 // "If state is false, set the selection's value to "subscript"."
michael@0 3613 if (!state) {
michael@0 3614 setSelectionValue("subscript", "subscript");
michael@0 3615 }
michael@0 3616
michael@0 3617 // "Return true."
michael@0 3618 return true;
michael@0 3619 }, indeterm: function() {
michael@0 3620 // "True if either among formattable nodes that are effectively
michael@0 3621 // contained in the active range, there is at least one with effective
michael@0 3622 // command value "subscript" and at least one with some other effective
michael@0 3623 // command value; or if there is some formattable node effectively
michael@0 3624 // contained in the active range with effective command value "mixed".
michael@0 3625 // Otherwise false."
michael@0 3626 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
michael@0 3627 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
michael@0 3628 && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
michael@0 3629 || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
michael@0 3630 }, inlineCommandActivatedValues: ["subscript"],
michael@0 3631 };
michael@0 3632
michael@0 3633 //@}
michael@0 3634 ///// The superscript command /////
michael@0 3635 //@{
michael@0 3636 commands.superscript = {
michael@0 3637 action: function() {
michael@0 3638 // "Call queryCommandState("superscript"), and let state be the
michael@0 3639 // result."
michael@0 3640 var state = myQueryCommandState("superscript");
michael@0 3641
michael@0 3642 // "Set the selection's value to null."
michael@0 3643 setSelectionValue("superscript", null);
michael@0 3644
michael@0 3645 // "If state is false, set the selection's value to "superscript"."
michael@0 3646 if (!state) {
michael@0 3647 setSelectionValue("superscript", "superscript");
michael@0 3648 }
michael@0 3649
michael@0 3650 // "Return true."
michael@0 3651 return true;
michael@0 3652 }, indeterm: function() {
michael@0 3653 // "True if either among formattable nodes that are effectively
michael@0 3654 // contained in the active range, there is at least one with effective
michael@0 3655 // command value "superscript" and at least one with some other
michael@0 3656 // effective command value; or if there is some formattable node
michael@0 3657 // effectively contained in the active range with effective command
michael@0 3658 // value "mixed". Otherwise false."
michael@0 3659 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
michael@0 3660 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
michael@0 3661 && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
michael@0 3662 || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
michael@0 3663 }, inlineCommandActivatedValues: ["superscript"],
michael@0 3664 };
michael@0 3665
michael@0 3666 //@}
michael@0 3667 ///// The underline command /////
michael@0 3668 //@{
michael@0 3669 commands.underline = {
michael@0 3670 action: function() {
michael@0 3671 // "If queryCommandState("underline") returns true, set the selection's
michael@0 3672 // value to null. Otherwise set the selection's value to "underline".
michael@0 3673 // Either way, return true."
michael@0 3674 if (myQueryCommandState("underline")) {
michael@0 3675 setSelectionValue("underline", null);
michael@0 3676 } else {
michael@0 3677 setSelectionValue("underline", "underline");
michael@0 3678 }
michael@0 3679 return true;
michael@0 3680 }, inlineCommandActivatedValues: ["underline"]
michael@0 3681 };
michael@0 3682
michael@0 3683 //@}
michael@0 3684 ///// The unlink command /////
michael@0 3685 //@{
michael@0 3686 commands.unlink = {
michael@0 3687 action: function() {
michael@0 3688 // "Let hyperlinks be a list of every a element that has an href
michael@0 3689 // attribute and is contained in the active range or is an ancestor of
michael@0 3690 // one of its boundary points."
michael@0 3691 //
michael@0 3692 // As usual, take care to ensure it's tree order. The correctness of
michael@0 3693 // the following is left as an exercise for the reader.
michael@0 3694 var range = getActiveRange();
michael@0 3695 var hyperlinks = [];
michael@0 3696 for (
michael@0 3697 var node = range.startContainer;
michael@0 3698 node;
michael@0 3699 node = node.parentNode
michael@0 3700 ) {
michael@0 3701 if (isHtmlElement(node, "A")
michael@0 3702 && node.hasAttribute("href")) {
michael@0 3703 hyperlinks.unshift(node);
michael@0 3704 }
michael@0 3705 }
michael@0 3706 for (
michael@0 3707 var node = range.startContainer;
michael@0 3708 node != nextNodeDescendants(range.endContainer);
michael@0 3709 node = nextNode(node)
michael@0 3710 ) {
michael@0 3711 if (isHtmlElement(node, "A")
michael@0 3712 && node.hasAttribute("href")
michael@0 3713 && (isContained(node, range)
michael@0 3714 || isAncestor(node, range.endContainer)
michael@0 3715 || node == range.endContainer)) {
michael@0 3716 hyperlinks.push(node);
michael@0 3717 }
michael@0 3718 }
michael@0 3719
michael@0 3720 // "Clear the value of each member of hyperlinks."
michael@0 3721 for (var i = 0; i < hyperlinks.length; i++) {
michael@0 3722 clearValue(hyperlinks[i], "unlink");
michael@0 3723 }
michael@0 3724
michael@0 3725 // "Return true."
michael@0 3726 return true;
michael@0 3727 }
michael@0 3728 };
michael@0 3729
michael@0 3730 //@}
michael@0 3731
michael@0 3732 /////////////////////////////////////
michael@0 3733 ///// Block formatting commands /////
michael@0 3734 /////////////////////////////////////
michael@0 3735
michael@0 3736 ///// Block formatting command definitions /////
michael@0 3737 //@{
michael@0 3738
michael@0 3739 // "An indentation element is either a blockquote, or a div that has a style
michael@0 3740 // attribute that sets "margin" or some subproperty of it."
michael@0 3741 function isIndentationElement(node) {
michael@0 3742 if (!isHtmlElement(node)) {
michael@0 3743 return false;
michael@0 3744 }
michael@0 3745
michael@0 3746 if (node.tagName == "BLOCKQUOTE") {
michael@0 3747 return true;
michael@0 3748 }
michael@0 3749
michael@0 3750 if (node.tagName != "DIV") {
michael@0 3751 return false;
michael@0 3752 }
michael@0 3753
michael@0 3754 for (var i = 0; i < node.style.length; i++) {
michael@0 3755 // Approximate check
michael@0 3756 if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
michael@0 3757 return true;
michael@0 3758 }
michael@0 3759 }
michael@0 3760
michael@0 3761 return false;
michael@0 3762 }
michael@0 3763
michael@0 3764 // "A simple indentation element is an indentation element that has no
michael@0 3765 // attributes except possibly
michael@0 3766 //
michael@0 3767 // * "a style attribute that sets no properties other than "margin",
michael@0 3768 // "border", "padding", or subproperties of those; and/or
michael@0 3769 // * "a dir attribute."
michael@0 3770 function isSimpleIndentationElement(node) {
michael@0 3771 if (!isIndentationElement(node)) {
michael@0 3772 return false;
michael@0 3773 }
michael@0 3774
michael@0 3775 for (var i = 0; i < node.attributes.length; i++) {
michael@0 3776 if (!isHtmlNamespace(node.attributes[i].namespaceURI)
michael@0 3777 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
michael@0 3778 return false;
michael@0 3779 }
michael@0 3780 }
michael@0 3781
michael@0 3782 for (var i = 0; i < node.style.length; i++) {
michael@0 3783 // This is approximate, but it works well enough for my purposes.
michael@0 3784 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
michael@0 3785 return false;
michael@0 3786 }
michael@0 3787 }
michael@0 3788
michael@0 3789 return true;
michael@0 3790 }
michael@0 3791
michael@0 3792 // "A non-list single-line container is an HTML element with local name
michael@0 3793 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
michael@0 3794 // or "xmp"."
michael@0 3795 function isNonListSingleLineContainer(node) {
michael@0 3796 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
michael@0 3797 "h6", "listing", "p", "pre", "xmp"]);
michael@0 3798 }
michael@0 3799
michael@0 3800 // "A single-line container is either a non-list single-line container, or an
michael@0 3801 // HTML element with local name "li", "dt", or "dd"."
michael@0 3802 function isSingleLineContainer(node) {
michael@0 3803 return isNonListSingleLineContainer(node)
michael@0 3804 || isHtmlElement(node, ["li", "dt", "dd"]);
michael@0 3805 }
michael@0 3806
michael@0 3807 function getBlockNodeOf(node) {
michael@0 3808 // "While node is an inline node, set node to its parent."
michael@0 3809 while (isInlineNode(node)) {
michael@0 3810 node = node.parentNode;
michael@0 3811 }
michael@0 3812
michael@0 3813 // "Return node."
michael@0 3814 return node;
michael@0 3815 }
michael@0 3816
michael@0 3817 //@}
michael@0 3818 ///// Assorted block formatting command algorithms /////
michael@0 3819 //@{
michael@0 3820
michael@0 3821 function fixDisallowedAncestors(node) {
michael@0 3822 // "If node is not editable, abort these steps."
michael@0 3823 if (!isEditable(node)) {
michael@0 3824 return;
michael@0 3825 }
michael@0 3826
michael@0 3827 // "If node is not an allowed child of any of its ancestors in the same
michael@0 3828 // editing host:"
michael@0 3829 if (getAncestors(node).every(function(ancestor) {
michael@0 3830 return !inSameEditingHost(node, ancestor)
michael@0 3831 || !isAllowedChild(node, ancestor)
michael@0 3832 })) {
michael@0 3833 // "If node is a dd or dt, wrap the one-node list consisting of node,
michael@0 3834 // with sibling criteria returning true for any dl with no attributes
michael@0 3835 // and false otherwise, and new parent instructions returning the
michael@0 3836 // result of calling createElement("dl") on the context object. Then
michael@0 3837 // abort these steps."
michael@0 3838 if (isHtmlElement(node, ["dd", "dt"])) {
michael@0 3839 wrap([node],
michael@0 3840 function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length },
michael@0 3841 function() { return document.createElement("dl") });
michael@0 3842 return;
michael@0 3843 }
michael@0 3844
michael@0 3845 // "If "p" is not an allowed child of the editing host of node, abort
michael@0 3846 // these steps."
michael@0 3847 if (!isAllowedChild("p", getEditingHostOf(node))) {
michael@0 3848 return;
michael@0 3849 }
michael@0 3850
michael@0 3851 // "If node is not a prohibited paragraph child, abort these steps."
michael@0 3852 if (!isProhibitedParagraphChild(node)) {
michael@0 3853 return;
michael@0 3854 }
michael@0 3855
michael@0 3856 // "Set the tag name of node to the default single-line container name,
michael@0 3857 // and let node be the result."
michael@0 3858 node = setTagName(node, defaultSingleLineContainerName);
michael@0 3859
michael@0 3860 // "Fix disallowed ancestors of node."
michael@0 3861 fixDisallowedAncestors(node);
michael@0 3862
michael@0 3863 // "Let children be node's children."
michael@0 3864 var children = [].slice.call(node.childNodes);
michael@0 3865
michael@0 3866 // "For each child in children, if child is a prohibited paragraph
michael@0 3867 // child:"
michael@0 3868 children.filter(isProhibitedParagraphChild)
michael@0 3869 .forEach(function(child) {
michael@0 3870 // "Record the values of the one-node list consisting of child, and
michael@0 3871 // let values be the result."
michael@0 3872 var values = recordValues([child]);
michael@0 3873
michael@0 3874 // "Split the parent of the one-node list consisting of child."
michael@0 3875 splitParent([child]);
michael@0 3876
michael@0 3877 // "Restore the values from values."
michael@0 3878 restoreValues(values);
michael@0 3879 });
michael@0 3880
michael@0 3881 // "Abort these steps."
michael@0 3882 return;
michael@0 3883 }
michael@0 3884
michael@0 3885 // "Record the values of the one-node list consisting of node, and let
michael@0 3886 // values be the result."
michael@0 3887 var values = recordValues([node]);
michael@0 3888
michael@0 3889 // "While node is not an allowed child of its parent, split the parent of
michael@0 3890 // the one-node list consisting of node."
michael@0 3891 while (!isAllowedChild(node, node.parentNode)) {
michael@0 3892 splitParent([node]);
michael@0 3893 }
michael@0 3894
michael@0 3895 // "Restore the values from values."
michael@0 3896 restoreValues(values);
michael@0 3897 }
michael@0 3898
michael@0 3899 function normalizeSublists(item) {
michael@0 3900 // "If item is not an li or it is not editable or its parent is not
michael@0 3901 // editable, abort these steps."
michael@0 3902 if (!isHtmlElement(item, "LI")
michael@0 3903 || !isEditable(item)
michael@0 3904 || !isEditable(item.parentNode)) {
michael@0 3905 return;
michael@0 3906 }
michael@0 3907
michael@0 3908 // "Let new item be null."
michael@0 3909 var newItem = null;
michael@0 3910
michael@0 3911 // "While item has an ol or ul child:"
michael@0 3912 while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) {
michael@0 3913 // "Let child be the last child of item."
michael@0 3914 var child = item.lastChild;
michael@0 3915
michael@0 3916 // "If child is an ol or ul, or new item is null and child is a Text
michael@0 3917 // node whose data consists of zero of more space characters:"
michael@0 3918 if (isHtmlElement(child, ["OL", "UL"])
michael@0 3919 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
michael@0 3920 // "Set new item to null."
michael@0 3921 newItem = null;
michael@0 3922
michael@0 3923 // "Insert child into the parent of item immediately following
michael@0 3924 // item, preserving ranges."
michael@0 3925 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item));
michael@0 3926
michael@0 3927 // "Otherwise:"
michael@0 3928 } else {
michael@0 3929 // "If new item is null, let new item be the result of calling
michael@0 3930 // createElement("li") on the ownerDocument of item, then insert
michael@0 3931 // new item into the parent of item immediately after item."
michael@0 3932 if (!newItem) {
michael@0 3933 newItem = item.ownerDocument.createElement("li");
michael@0 3934 item.parentNode.insertBefore(newItem, item.nextSibling);
michael@0 3935 }
michael@0 3936
michael@0 3937 // "Insert child into new item as its first child, preserving
michael@0 3938 // ranges."
michael@0 3939 movePreservingRanges(child, newItem, 0);
michael@0 3940 }
michael@0 3941 }
michael@0 3942 }
michael@0 3943
michael@0 3944 function getSelectionListState() {
michael@0 3945 // "If the active range is null, return "none"."
michael@0 3946 if (!getActiveRange()) {
michael@0 3947 return "none";
michael@0 3948 }
michael@0 3949
michael@0 3950 // "Block-extend the active range, and let new range be the result."
michael@0 3951 var newRange = blockExtend(getActiveRange());
michael@0 3952
michael@0 3953 // "Let node list be a list of nodes, initially empty."
michael@0 3954 //
michael@0 3955 // "For each node contained in new range, append node to node list if the
michael@0 3956 // last member of node list (if any) is not an ancestor of node; node is
michael@0 3957 // editable; node is not an indentation element; and node is either an ol
michael@0 3958 // or ul, or the child of an ol or ul, or an allowed child of "li"."
michael@0 3959 var nodeList = getContainedNodes(newRange, function(node) {
michael@0 3960 return isEditable(node)
michael@0 3961 && !isIndentationElement(node)
michael@0 3962 && (isHtmlElement(node, ["ol", "ul"])
michael@0 3963 || isHtmlElement(node.parentNode, ["ol", "ul"])
michael@0 3964 || isAllowedChild(node, "li"));
michael@0 3965 });
michael@0 3966
michael@0 3967 // "If node list is empty, return "none"."
michael@0 3968 if (!nodeList.length) {
michael@0 3969 return "none";
michael@0 3970 }
michael@0 3971
michael@0 3972 // "If every member of node list is either an ol or the child of an ol or
michael@0 3973 // the child of an li child of an ol, and none is a ul or an ancestor of a
michael@0 3974 // ul, return "ol"."
michael@0 3975 if (nodeList.every(function(node) {
michael@0 3976 return isHtmlElement(node, "ol")
michael@0 3977 || isHtmlElement(node.parentNode, "ol")
michael@0 3978 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
michael@0 3979 })
michael@0 3980 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) {
michael@0 3981 return "ol";
michael@0 3982 }
michael@0 3983
michael@0 3984 // "If every member of node list is either a ul or the child of a ul or the
michael@0 3985 // child of an li child of a ul, and none is an ol or an ancestor of an ol,
michael@0 3986 // return "ul"."
michael@0 3987 if (nodeList.every(function(node) {
michael@0 3988 return isHtmlElement(node, "ul")
michael@0 3989 || isHtmlElement(node.parentNode, "ul")
michael@0 3990 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
michael@0 3991 })
michael@0 3992 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) {
michael@0 3993 return "ul";
michael@0 3994 }
michael@0 3995
michael@0 3996 var hasOl = nodeList.some(function(node) {
michael@0 3997 return isHtmlElement(node, "ol")
michael@0 3998 || isHtmlElement(node.parentNode, "ol")
michael@0 3999 || ("querySelector" in node && node.querySelector("ol"))
michael@0 4000 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
michael@0 4001 });
michael@0 4002 var hasUl = nodeList.some(function(node) {
michael@0 4003 return isHtmlElement(node, "ul")
michael@0 4004 || isHtmlElement(node.parentNode, "ul")
michael@0 4005 || ("querySelector" in node && node.querySelector("ul"))
michael@0 4006 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
michael@0 4007 });
michael@0 4008 // "If some member of node list is either an ol or the child or ancestor of
michael@0 4009 // an ol or the child of an li child of an ol, and some member of node list
michael@0 4010 // is either a ul or the child or ancestor of a ul or the child of an li
michael@0 4011 // child of a ul, return "mixed"."
michael@0 4012 if (hasOl && hasUl) {
michael@0 4013 return "mixed";
michael@0 4014 }
michael@0 4015
michael@0 4016 // "If some member of node list is either an ol or the child or ancestor of
michael@0 4017 // an ol or the child of an li child of an ol, return "mixed ol"."
michael@0 4018 if (hasOl) {
michael@0 4019 return "mixed ol";
michael@0 4020 }
michael@0 4021
michael@0 4022 // "If some member of node list is either a ul or the child or ancestor of
michael@0 4023 // a ul or the child of an li child of a ul, return "mixed ul"."
michael@0 4024 if (hasUl) {
michael@0 4025 return "mixed ul";
michael@0 4026 }
michael@0 4027
michael@0 4028 // "Return "none"."
michael@0 4029 return "none";
michael@0 4030 }
michael@0 4031
michael@0 4032 function getAlignmentValue(node) {
michael@0 4033 // "While node is neither null nor an Element, or it is an Element but its
michael@0 4034 // "display" property has resolved value "inline" or "none", set node to
michael@0 4035 // its parent."
michael@0 4036 while ((node && node.nodeType != Node.ELEMENT_NODE)
michael@0 4037 || (node.nodeType == Node.ELEMENT_NODE
michael@0 4038 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
michael@0 4039 node = node.parentNode;
michael@0 4040 }
michael@0 4041
michael@0 4042 // "If node is not an Element, return "left"."
michael@0 4043 if (!node || node.nodeType != Node.ELEMENT_NODE) {
michael@0 4044 return "left";
michael@0 4045 }
michael@0 4046
michael@0 4047 var resolvedValue = getComputedStyle(node).textAlign
michael@0 4048 // Hack around browser non-standardness
michael@0 4049 .replace(/^-(moz|webkit)-/, "")
michael@0 4050 .replace(/^auto$/, "start");
michael@0 4051
michael@0 4052 // "If node's "text-align" property has resolved value "start", return
michael@0 4053 // "left" if the directionality of node is "ltr", "right" if it is "rtl"."
michael@0 4054 if (resolvedValue == "start") {
michael@0 4055 return getDirectionality(node) == "ltr" ? "left" : "right";
michael@0 4056 }
michael@0 4057
michael@0 4058 // "If node's "text-align" property has resolved value "end", return
michael@0 4059 // "right" if the directionality of node is "ltr", "left" if it is "rtl"."
michael@0 4060 if (resolvedValue == "end") {
michael@0 4061 return getDirectionality(node) == "ltr" ? "right" : "left";
michael@0 4062 }
michael@0 4063
michael@0 4064 // "If node's "text-align" property has resolved value "center", "justify",
michael@0 4065 // "left", or "right", return that value."
michael@0 4066 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
michael@0 4067 return resolvedValue;
michael@0 4068 }
michael@0 4069
michael@0 4070 // "Return "left"."
michael@0 4071 return "left";
michael@0 4072 }
michael@0 4073
michael@0 4074 function getNextEquivalentPoint(node, offset) {
michael@0 4075 // "If node's length is zero, return null."
michael@0 4076 if (getNodeLength(node) == 0) {
michael@0 4077 return null;
michael@0 4078 }
michael@0 4079
michael@0 4080 // "If offset is node's length, and node's parent is not null, and node is
michael@0 4081 // an inline node, return (node's parent, 1 + node's index)."
michael@0 4082 if (offset == getNodeLength(node)
michael@0 4083 && node.parentNode
michael@0 4084 && isInlineNode(node)) {
michael@0 4085 return [node.parentNode, 1 + getNodeIndex(node)];
michael@0 4086 }
michael@0 4087
michael@0 4088 // "If node has a child with index offset, and that child's length is not
michael@0 4089 // zero, and that child is an inline node, return (that child, 0)."
michael@0 4090 if (0 <= offset
michael@0 4091 && offset < node.childNodes.length
michael@0 4092 && getNodeLength(node.childNodes[offset]) != 0
michael@0 4093 && isInlineNode(node.childNodes[offset])) {
michael@0 4094 return [node.childNodes[offset], 0];
michael@0 4095 }
michael@0 4096
michael@0 4097 // "Return null."
michael@0 4098 return null;
michael@0 4099 }
michael@0 4100
michael@0 4101 function getPreviousEquivalentPoint(node, offset) {
michael@0 4102 // "If node's length is zero, return null."
michael@0 4103 if (getNodeLength(node) == 0) {
michael@0 4104 return null;
michael@0 4105 }
michael@0 4106
michael@0 4107 // "If offset is 0, and node's parent is not null, and node is an inline
michael@0 4108 // node, return (node's parent, node's index)."
michael@0 4109 if (offset == 0
michael@0 4110 && node.parentNode
michael@0 4111 && isInlineNode(node)) {
michael@0 4112 return [node.parentNode, getNodeIndex(node)];
michael@0 4113 }
michael@0 4114
michael@0 4115 // "If node has a child with index offset − 1, and that child's length is
michael@0 4116 // not zero, and that child is an inline node, return (that child, that
michael@0 4117 // child's length)."
michael@0 4118 if (0 <= offset - 1
michael@0 4119 && offset - 1 < node.childNodes.length
michael@0 4120 && getNodeLength(node.childNodes[offset - 1]) != 0
michael@0 4121 && isInlineNode(node.childNodes[offset - 1])) {
michael@0 4122 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
michael@0 4123 }
michael@0 4124
michael@0 4125 // "Return null."
michael@0 4126 return null;
michael@0 4127 }
michael@0 4128
michael@0 4129 function getFirstEquivalentPoint(node, offset) {
michael@0 4130 // "While (node, offset)'s previous equivalent point is not null, set
michael@0 4131 // (node, offset) to its previous equivalent point."
michael@0 4132 var prev;
michael@0 4133 while (prev = getPreviousEquivalentPoint(node, offset)) {
michael@0 4134 node = prev[0];
michael@0 4135 offset = prev[1];
michael@0 4136 }
michael@0 4137
michael@0 4138 // "Return (node, offset)."
michael@0 4139 return [node, offset];
michael@0 4140 }
michael@0 4141
michael@0 4142 function getLastEquivalentPoint(node, offset) {
michael@0 4143 // "While (node, offset)'s next equivalent point is not null, set (node,
michael@0 4144 // offset) to its next equivalent point."
michael@0 4145 var next;
michael@0 4146 while (next = getNextEquivalentPoint(node, offset)) {
michael@0 4147 node = next[0];
michael@0 4148 offset = next[1];
michael@0 4149 }
michael@0 4150
michael@0 4151 // "Return (node, offset)."
michael@0 4152 return [node, offset];
michael@0 4153 }
michael@0 4154
michael@0 4155 //@}
michael@0 4156 ///// Block-extending a range /////
michael@0 4157 //@{
michael@0 4158
michael@0 4159 // "A boundary point (node, offset) is a block start point if either node's
michael@0 4160 // parent is null and offset is zero; or node has a child with index offset −
michael@0 4161 // 1, and that child is either a visible block node or a visible br."
michael@0 4162 function isBlockStartPoint(node, offset) {
michael@0 4163 return (!node.parentNode && offset == 0)
michael@0 4164 || (0 <= offset - 1
michael@0 4165 && offset - 1 < node.childNodes.length
michael@0 4166 && isVisible(node.childNodes[offset - 1])
michael@0 4167 && (isBlockNode(node.childNodes[offset - 1])
michael@0 4168 || isHtmlElement(node.childNodes[offset - 1], "br")));
michael@0 4169 }
michael@0 4170
michael@0 4171 // "A boundary point (node, offset) is a block end point if either node's
michael@0 4172 // parent is null and offset is node's length; or node has a child with index
michael@0 4173 // offset, and that child is a visible block node."
michael@0 4174 function isBlockEndPoint(node, offset) {
michael@0 4175 return (!node.parentNode && offset == getNodeLength(node))
michael@0 4176 || (offset < node.childNodes.length
michael@0 4177 && isVisible(node.childNodes[offset])
michael@0 4178 && isBlockNode(node.childNodes[offset]));
michael@0 4179 }
michael@0 4180
michael@0 4181 // "A boundary point is a block boundary point if it is either a block start
michael@0 4182 // point or a block end point."
michael@0 4183 function isBlockBoundaryPoint(node, offset) {
michael@0 4184 return isBlockStartPoint(node, offset)
michael@0 4185 || isBlockEndPoint(node, offset);
michael@0 4186 }
michael@0 4187
michael@0 4188 function blockExtend(range) {
michael@0 4189 // "Let start node, start offset, end node, and end offset be the start
michael@0 4190 // and end nodes and offsets of the range."
michael@0 4191 var startNode = range.startContainer;
michael@0 4192 var startOffset = range.startOffset;
michael@0 4193 var endNode = range.endContainer;
michael@0 4194 var endOffset = range.endOffset;
michael@0 4195
michael@0 4196 // "If some ancestor container of start node is an li, set start offset to
michael@0 4197 // the index of the last such li in tree order, and set start node to that
michael@0 4198 // li's parent."
michael@0 4199 var liAncestors = getAncestors(startNode).concat(startNode)
michael@0 4200 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
michael@0 4201 .slice(-1);
michael@0 4202 if (liAncestors.length) {
michael@0 4203 startOffset = getNodeIndex(liAncestors[0]);
michael@0 4204 startNode = liAncestors[0].parentNode;
michael@0 4205 }
michael@0 4206
michael@0 4207 // "If (start node, start offset) is not a block start point, repeat the
michael@0 4208 // following steps:"
michael@0 4209 if (!isBlockStartPoint(startNode, startOffset)) do {
michael@0 4210 // "If start offset is zero, set it to start node's index, then set
michael@0 4211 // start node to its parent."
michael@0 4212 if (startOffset == 0) {
michael@0 4213 startOffset = getNodeIndex(startNode);
michael@0 4214 startNode = startNode.parentNode;
michael@0 4215
michael@0 4216 // "Otherwise, subtract one from start offset."
michael@0 4217 } else {
michael@0 4218 startOffset--;
michael@0 4219 }
michael@0 4220
michael@0 4221 // "If (start node, start offset) is a block boundary point, break from
michael@0 4222 // this loop."
michael@0 4223 } while (!isBlockBoundaryPoint(startNode, startOffset));
michael@0 4224
michael@0 4225 // "While start offset is zero and start node's parent is not null, set
michael@0 4226 // start offset to start node's index, then set start node to its parent."
michael@0 4227 while (startOffset == 0
michael@0 4228 && startNode.parentNode) {
michael@0 4229 startOffset = getNodeIndex(startNode);
michael@0 4230 startNode = startNode.parentNode;
michael@0 4231 }
michael@0 4232
michael@0 4233 // "If some ancestor container of end node is an li, set end offset to one
michael@0 4234 // plus the index of the last such li in tree order, and set end node to
michael@0 4235 // that li's parent."
michael@0 4236 var liAncestors = getAncestors(endNode).concat(endNode)
michael@0 4237 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
michael@0 4238 .slice(-1);
michael@0 4239 if (liAncestors.length) {
michael@0 4240 endOffset = 1 + getNodeIndex(liAncestors[0]);
michael@0 4241 endNode = liAncestors[0].parentNode;
michael@0 4242 }
michael@0 4243
michael@0 4244 // "If (end node, end offset) is not a block end point, repeat the
michael@0 4245 // following steps:"
michael@0 4246 if (!isBlockEndPoint(endNode, endOffset)) do {
michael@0 4247 // "If end offset is end node's length, set it to one plus end node's
michael@0 4248 // index, then set end node to its parent."
michael@0 4249 if (endOffset == getNodeLength(endNode)) {
michael@0 4250 endOffset = 1 + getNodeIndex(endNode);
michael@0 4251 endNode = endNode.parentNode;
michael@0 4252
michael@0 4253 // "Otherwise, add one to end offset.
michael@0 4254 } else {
michael@0 4255 endOffset++;
michael@0 4256 }
michael@0 4257
michael@0 4258 // "If (end node, end offset) is a block boundary point, break from
michael@0 4259 // this loop."
michael@0 4260 } while (!isBlockBoundaryPoint(endNode, endOffset));
michael@0 4261
michael@0 4262 // "While end offset is end node's length and end node's parent is not
michael@0 4263 // null, set end offset to one plus end node's index, then set end node to
michael@0 4264 // its parent."
michael@0 4265 while (endOffset == getNodeLength(endNode)
michael@0 4266 && endNode.parentNode) {
michael@0 4267 endOffset = 1 + getNodeIndex(endNode);
michael@0 4268 endNode = endNode.parentNode;
michael@0 4269 }
michael@0 4270
michael@0 4271 // "Let new range be a new range whose start and end nodes and offsets
michael@0 4272 // are start node, start offset, end node, and end offset."
michael@0 4273 var newRange = startNode.ownerDocument.createRange();
michael@0 4274 newRange.setStart(startNode, startOffset);
michael@0 4275 newRange.setEnd(endNode, endOffset);
michael@0 4276
michael@0 4277 // "Return new range."
michael@0 4278 return newRange;
michael@0 4279 }
michael@0 4280
michael@0 4281 function followsLineBreak(node) {
michael@0 4282 // "Let offset be zero."
michael@0 4283 var offset = 0;
michael@0 4284
michael@0 4285 // "While (node, offset) is not a block boundary point:"
michael@0 4286 while (!isBlockBoundaryPoint(node, offset)) {
michael@0 4287 // "If node has a visible child with index offset minus one, return
michael@0 4288 // false."
michael@0 4289 if (0 <= offset - 1
michael@0 4290 && offset - 1 < node.childNodes.length
michael@0 4291 && isVisible(node.childNodes[offset - 1])) {
michael@0 4292 return false;
michael@0 4293 }
michael@0 4294
michael@0 4295 // "If offset is zero or node has no children, set offset to node's
michael@0 4296 // index, then set node to its parent."
michael@0 4297 if (offset == 0
michael@0 4298 || !node.hasChildNodes()) {
michael@0 4299 offset = getNodeIndex(node);
michael@0 4300 node = node.parentNode;
michael@0 4301
michael@0 4302 // "Otherwise, set node to its child with index offset minus one, then
michael@0 4303 // set offset to node's length."
michael@0 4304 } else {
michael@0 4305 node = node.childNodes[offset - 1];
michael@0 4306 offset = getNodeLength(node);
michael@0 4307 }
michael@0 4308 }
michael@0 4309
michael@0 4310 // "Return true."
michael@0 4311 return true;
michael@0 4312 }
michael@0 4313
michael@0 4314 function precedesLineBreak(node) {
michael@0 4315 // "Let offset be node's length."
michael@0 4316 var offset = getNodeLength(node);
michael@0 4317
michael@0 4318 // "While (node, offset) is not a block boundary point:"
michael@0 4319 while (!isBlockBoundaryPoint(node, offset)) {
michael@0 4320 // "If node has a visible child with index offset, return false."
michael@0 4321 if (offset < node.childNodes.length
michael@0 4322 && isVisible(node.childNodes[offset])) {
michael@0 4323 return false;
michael@0 4324 }
michael@0 4325
michael@0 4326 // "If offset is node's length or node has no children, set offset to
michael@0 4327 // one plus node's index, then set node to its parent."
michael@0 4328 if (offset == getNodeLength(node)
michael@0 4329 || !node.hasChildNodes()) {
michael@0 4330 offset = 1 + getNodeIndex(node);
michael@0 4331 node = node.parentNode;
michael@0 4332
michael@0 4333 // "Otherwise, set node to its child with index offset and set offset
michael@0 4334 // to zero."
michael@0 4335 } else {
michael@0 4336 node = node.childNodes[offset];
michael@0 4337 offset = 0;
michael@0 4338 }
michael@0 4339 }
michael@0 4340
michael@0 4341 // "Return true."
michael@0 4342 return true;
michael@0 4343 }
michael@0 4344
michael@0 4345 //@}
michael@0 4346 ///// Recording and restoring overrides /////
michael@0 4347 //@{
michael@0 4348
michael@0 4349 function recordCurrentOverrides() {
michael@0 4350 // "Let overrides be a list of (string, string or boolean) ordered pairs,
michael@0 4351 // initially empty."
michael@0 4352 var overrides = [];
michael@0 4353
michael@0 4354 // "If there is a value override for "createLink", add ("createLink", value
michael@0 4355 // override for "createLink") to overrides."
michael@0 4356 if (getValueOverride("createlink") !== undefined) {
michael@0 4357 overrides.push(["createlink", getValueOverride("createlink")]);
michael@0 4358 }
michael@0 4359
michael@0 4360 // "For each command in the list "bold", "italic", "strikethrough",
michael@0 4361 // "subscript", "superscript", "underline", in order: if there is a state
michael@0 4362 // override for command, add (command, command's state override) to
michael@0 4363 // overrides."
michael@0 4364 ["bold", "italic", "strikethrough", "subscript", "superscript",
michael@0 4365 "underline"].forEach(function(command) {
michael@0 4366 if (getStateOverride(command) !== undefined) {
michael@0 4367 overrides.push([command, getStateOverride(command)]);
michael@0 4368 }
michael@0 4369 });
michael@0 4370
michael@0 4371 // "For each command in the list "fontName", "fontSize", "foreColor",
michael@0 4372 // "hiliteColor", in order: if there is a value override for command, add
michael@0 4373 // (command, command's value override) to overrides."
michael@0 4374 ["fontname", "fontsize", "forecolor",
michael@0 4375 "hilitecolor"].forEach(function(command) {
michael@0 4376 if (getValueOverride(command) !== undefined) {
michael@0 4377 overrides.push([command, getValueOverride(command)]);
michael@0 4378 }
michael@0 4379 });
michael@0 4380
michael@0 4381 // "Return overrides."
michael@0 4382 return overrides;
michael@0 4383 }
michael@0 4384
michael@0 4385 function recordCurrentStatesAndValues() {
michael@0 4386 // "Let overrides be a list of (string, string or boolean) ordered pairs,
michael@0 4387 // initially empty."
michael@0 4388 var overrides = [];
michael@0 4389
michael@0 4390 // "Let node be the first formattable node effectively contained in the
michael@0 4391 // active range, or null if there is none."
michael@0 4392 var node = getAllEffectivelyContainedNodes(getActiveRange())
michael@0 4393 .filter(isFormattableNode)[0];
michael@0 4394
michael@0 4395 // "If node is null, return overrides."
michael@0 4396 if (!node) {
michael@0 4397 return overrides;
michael@0 4398 }
michael@0 4399
michael@0 4400 // "Add ("createLink", node's effective command value for "createLink") to
michael@0 4401 // overrides."
michael@0 4402 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]);
michael@0 4403
michael@0 4404 // "For each command in the list "bold", "italic", "strikethrough",
michael@0 4405 // "subscript", "superscript", "underline", in order: if node's effective
michael@0 4406 // command value for command is one of its inline command activated values,
michael@0 4407 // add (command, true) to overrides, and otherwise add (command, false) to
michael@0 4408 // overrides."
michael@0 4409 ["bold", "italic", "strikethrough", "subscript", "superscript",
michael@0 4410 "underline"].forEach(function(command) {
michael@0 4411 if (commands[command].inlineCommandActivatedValues
michael@0 4412 .indexOf(getEffectiveCommandValue(node, command)) != -1) {
michael@0 4413 overrides.push([command, true]);
michael@0 4414 } else {
michael@0 4415 overrides.push([command, false]);
michael@0 4416 }
michael@0 4417 });
michael@0 4418
michael@0 4419 // "For each command in the list "fontName", "foreColor", "hiliteColor", in
michael@0 4420 // order: add (command, command's value) to overrides."
michael@0 4421 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
michael@0 4422 overrides.push([command, commands[command].value()]);
michael@0 4423 });
michael@0 4424
michael@0 4425 // "Add ("fontSize", node's effective command value for "fontSize") to
michael@0 4426 // overrides."
michael@0 4427 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
michael@0 4428
michael@0 4429 // "Return overrides."
michael@0 4430 return overrides;
michael@0 4431 }
michael@0 4432
michael@0 4433 function restoreStatesAndValues(overrides) {
michael@0 4434 // "Let node be the first formattable node effectively contained in the
michael@0 4435 // active range, or null if there is none."
michael@0 4436 var node = getAllEffectivelyContainedNodes(getActiveRange())
michael@0 4437 .filter(isFormattableNode)[0];
michael@0 4438
michael@0 4439 // "If node is not null, then for each (command, override) pair in
michael@0 4440 // overrides, in order:"
michael@0 4441 if (node) {
michael@0 4442 for (var i = 0; i < overrides.length; i++) {
michael@0 4443 var command = overrides[i][0];
michael@0 4444 var override = overrides[i][1];
michael@0 4445
michael@0 4446 // "If override is a boolean, and queryCommandState(command)
michael@0 4447 // returns something different from override, take the action for
michael@0 4448 // command, with value equal to the empty string."
michael@0 4449 if (typeof override == "boolean"
michael@0 4450 && myQueryCommandState(command) != override) {
michael@0 4451 commands[command].action("");
michael@0 4452
michael@0 4453 // "Otherwise, if override is a string, and command is neither
michael@0 4454 // "createLink" nor "fontSize", and queryCommandValue(command)
michael@0 4455 // returns something not equivalent to override, take the action
michael@0 4456 // for command, with value equal to override."
michael@0 4457 } else if (typeof override == "string"
michael@0 4458 && command != "createlink"
michael@0 4459 && command != "fontsize"
michael@0 4460 && !areEquivalentValues(command, myQueryCommandValue(command), override)) {
michael@0 4461 commands[command].action(override);
michael@0 4462
michael@0 4463 // "Otherwise, if override is a string; and command is
michael@0 4464 // "createLink"; and either there is a value override for
michael@0 4465 // "createLink" that is not equal to override, or there is no value
michael@0 4466 // override for "createLink" and node's effective command value for
michael@0 4467 // "createLink" is not equal to override: take the action for
michael@0 4468 // "createLink", with value equal to override."
michael@0 4469 } else if (typeof override == "string"
michael@0 4470 && command == "createlink"
michael@0 4471 && (
michael@0 4472 (
michael@0 4473 getValueOverride("createlink") !== undefined
michael@0 4474 && getValueOverride("createlink") !== override
michael@0 4475 ) || (
michael@0 4476 getValueOverride("createlink") === undefined
michael@0 4477 && getEffectiveCommandValue(node, "createlink") !== override
michael@0 4478 )
michael@0 4479 )) {
michael@0 4480 commands.createlink.action(override);
michael@0 4481
michael@0 4482 // "Otherwise, if override is a string; and command is "fontSize";
michael@0 4483 // and either there is a value override for "fontSize" that is not
michael@0 4484 // equal to override, or there is no value override for "fontSize"
michael@0 4485 // and node's effective command value for "fontSize" is not loosely
michael@0 4486 // equivalent to override:"
michael@0 4487 } else if (typeof override == "string"
michael@0 4488 && command == "fontsize"
michael@0 4489 && (
michael@0 4490 (
michael@0 4491 getValueOverride("fontsize") !== undefined
michael@0 4492 && getValueOverride("fontsize") !== override
michael@0 4493 ) || (
michael@0 4494 getValueOverride("fontsize") === undefined
michael@0 4495 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
michael@0 4496 )
michael@0 4497 )) {
michael@0 4498 // "Convert override to an integer number of pixels, and set
michael@0 4499 // override to the legacy font size for the result."
michael@0 4500 override = getLegacyFontSize(override);
michael@0 4501
michael@0 4502 // "Take the action for "fontSize", with value equal to
michael@0 4503 // override."
michael@0 4504 commands.fontsize.action(override);
michael@0 4505
michael@0 4506 // "Otherwise, continue this loop from the beginning."
michael@0 4507 } else {
michael@0 4508 continue;
michael@0 4509 }
michael@0 4510
michael@0 4511 // "Set node to the first formattable node effectively contained in
michael@0 4512 // the active range, if there is one."
michael@0 4513 node = getAllEffectivelyContainedNodes(getActiveRange())
michael@0 4514 .filter(isFormattableNode)[0]
michael@0 4515 || node;
michael@0 4516 }
michael@0 4517
michael@0 4518 // "Otherwise, for each (command, override) pair in overrides, in order:"
michael@0 4519 } else {
michael@0 4520 for (var i = 0; i < overrides.length; i++) {
michael@0 4521 var command = overrides[i][0];
michael@0 4522 var override = overrides[i][1];
michael@0 4523
michael@0 4524 // "If override is a boolean, set the state override for command to
michael@0 4525 // override."
michael@0 4526 if (typeof override == "boolean") {
michael@0 4527 setStateOverride(command, override);
michael@0 4528 }
michael@0 4529
michael@0 4530 // "If override is a string, set the value override for command to
michael@0 4531 // override."
michael@0 4532 if (typeof override == "string") {
michael@0 4533 setValueOverride(command, override);
michael@0 4534 }
michael@0 4535 }
michael@0 4536 }
michael@0 4537 }
michael@0 4538
michael@0 4539 //@}
michael@0 4540 ///// Deleting the selection /////
michael@0 4541 //@{
michael@0 4542
michael@0 4543 // The flags argument is a dictionary that can have blockMerging,
michael@0 4544 // stripWrappers, and/or direction as keys.
michael@0 4545 function deleteSelection(flags) {
michael@0 4546 if (flags === undefined) {
michael@0 4547 flags = {};
michael@0 4548 }
michael@0 4549
michael@0 4550 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true;
michael@0 4551 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
michael@0 4552 var direction = "direction" in flags ? flags.direction : "forward";
michael@0 4553
michael@0 4554 // "If the active range is null, abort these steps and do nothing."
michael@0 4555 if (!getActiveRange()) {
michael@0 4556 return;
michael@0 4557 }
michael@0 4558
michael@0 4559 // "Canonicalize whitespace at the active range's start."
michael@0 4560 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 4561
michael@0 4562 // "Canonicalize whitespace at the active range's end."
michael@0 4563 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset);
michael@0 4564
michael@0 4565 // "Let (start node, start offset) be the last equivalent point for the
michael@0 4566 // active range's start."
michael@0 4567 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 4568 var startNode = start[0];
michael@0 4569 var startOffset = start[1];
michael@0 4570
michael@0 4571 // "Let (end node, end offset) be the first equivalent point for the active
michael@0 4572 // range's end."
michael@0 4573 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset);
michael@0 4574 var endNode = end[0];
michael@0 4575 var endOffset = end[1];
michael@0 4576
michael@0 4577 // "If (end node, end offset) is not after (start node, start offset):"
michael@0 4578 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
michael@0 4579 // "If direction is "forward", call collapseToStart() on the context
michael@0 4580 // object's Selection."
michael@0 4581 //
michael@0 4582 // Here and in a few other places, we check rangeCount to work around a
michael@0 4583 // WebKit bug: it will sometimes incorrectly remove ranges from the
michael@0 4584 // selection if nodes are removed, so collapseToStart() will throw.
michael@0 4585 // This will break everything if we're using an actual selection, but
michael@0 4586 // if getActiveRange() is really just returning globalRange and that's
michael@0 4587 // all we care about, it will work fine. I only add the extra check
michael@0 4588 // for errors I actually hit in testing.
michael@0 4589 if (direction == "forward") {
michael@0 4590 if (getSelection().rangeCount) {
michael@0 4591 getSelection().collapseToStart();
michael@0 4592 }
michael@0 4593 getActiveRange().collapse(true);
michael@0 4594
michael@0 4595 // "Otherwise, call collapseToEnd() on the context object's Selection."
michael@0 4596 } else {
michael@0 4597 getSelection().collapseToEnd();
michael@0 4598 getActiveRange().collapse(false);
michael@0 4599 }
michael@0 4600
michael@0 4601 // "Abort these steps."
michael@0 4602 return;
michael@0 4603 }
michael@0 4604
michael@0 4605 // "If start node is a Text node and start offset is 0, set start offset to
michael@0 4606 // the index of start node, then set start node to its parent."
michael@0 4607 if (startNode.nodeType == Node.TEXT_NODE
michael@0 4608 && startOffset == 0) {
michael@0 4609 startOffset = getNodeIndex(startNode);
michael@0 4610 startNode = startNode.parentNode;
michael@0 4611 }
michael@0 4612
michael@0 4613 // "If end node is a Text node and end offset is its length, set end offset
michael@0 4614 // to one plus the index of end node, then set end node to its parent."
michael@0 4615 if (endNode.nodeType == Node.TEXT_NODE
michael@0 4616 && endOffset == getNodeLength(endNode)) {
michael@0 4617 endOffset = 1 + getNodeIndex(endNode);
michael@0 4618 endNode = endNode.parentNode;
michael@0 4619 }
michael@0 4620
michael@0 4621 // "Call collapse(start node, start offset) on the context object's
michael@0 4622 // Selection."
michael@0 4623 getSelection().collapse(startNode, startOffset);
michael@0 4624 getActiveRange().setStart(startNode, startOffset);
michael@0 4625
michael@0 4626 // "Call extend(end node, end offset) on the context object's Selection."
michael@0 4627 getSelection().extend(endNode, endOffset);
michael@0 4628 getActiveRange().setEnd(endNode, endOffset);
michael@0 4629
michael@0 4630 // "Let start block be the active range's start node."
michael@0 4631 var startBlock = getActiveRange().startContainer;
michael@0 4632
michael@0 4633 // "While start block's parent is in the same editing host and start block
michael@0 4634 // is an inline node, set start block to its parent."
michael@0 4635 while (inSameEditingHost(startBlock, startBlock.parentNode)
michael@0 4636 && isInlineNode(startBlock)) {
michael@0 4637 startBlock = startBlock.parentNode;
michael@0 4638 }
michael@0 4639
michael@0 4640 // "If start block is neither a block node nor an editing host, or "span"
michael@0 4641 // is not an allowed child of start block, or start block is a td or th,
michael@0 4642 // set start block to null."
michael@0 4643 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
michael@0 4644 || !isAllowedChild("span", startBlock)
michael@0 4645 || isHtmlElement(startBlock, ["td", "th"])) {
michael@0 4646 startBlock = null;
michael@0 4647 }
michael@0 4648
michael@0 4649 // "Let end block be the active range's end node."
michael@0 4650 var endBlock = getActiveRange().endContainer;
michael@0 4651
michael@0 4652 // "While end block's parent is in the same editing host and end block is
michael@0 4653 // an inline node, set end block to its parent."
michael@0 4654 while (inSameEditingHost(endBlock, endBlock.parentNode)
michael@0 4655 && isInlineNode(endBlock)) {
michael@0 4656 endBlock = endBlock.parentNode;
michael@0 4657 }
michael@0 4658
michael@0 4659 // "If end block is neither a block node nor an editing host, or "span" is
michael@0 4660 // not an allowed child of end block, or end block is a td or th, set end
michael@0 4661 // block to null."
michael@0 4662 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
michael@0 4663 || !isAllowedChild("span", endBlock)
michael@0 4664 || isHtmlElement(endBlock, ["td", "th"])) {
michael@0 4665 endBlock = null;
michael@0 4666 }
michael@0 4667
michael@0 4668 // "Record current states and values, and let overrides be the result."
michael@0 4669 var overrides = recordCurrentStatesAndValues();
michael@0 4670
michael@0 4671 // "If start node and end node are the same, and start node is an editable
michael@0 4672 // Text node:"
michael@0 4673 if (startNode == endNode
michael@0 4674 && isEditable(startNode)
michael@0 4675 && startNode.nodeType == Node.TEXT_NODE) {
michael@0 4676 // "Call deleteData(start offset, end offset − start offset) on start
michael@0 4677 // node."
michael@0 4678 startNode.deleteData(startOffset, endOffset - startOffset);
michael@0 4679
michael@0 4680 // "Canonicalize whitespace at (start node, start offset), with fix
michael@0 4681 // collapsed space false."
michael@0 4682 canonicalizeWhitespace(startNode, startOffset, false);
michael@0 4683
michael@0 4684 // "If direction is "forward", call collapseToStart() on the context
michael@0 4685 // object's Selection."
michael@0 4686 if (direction == "forward") {
michael@0 4687 if (getSelection().rangeCount) {
michael@0 4688 getSelection().collapseToStart();
michael@0 4689 }
michael@0 4690 getActiveRange().collapse(true);
michael@0 4691
michael@0 4692 // "Otherwise, call collapseToEnd() on the context object's Selection."
michael@0 4693 } else {
michael@0 4694 getSelection().collapseToEnd();
michael@0 4695 getActiveRange().collapse(false);
michael@0 4696 }
michael@0 4697
michael@0 4698 // "Restore states and values from overrides."
michael@0 4699 restoreStatesAndValues(overrides);
michael@0 4700
michael@0 4701 // "Abort these steps."
michael@0 4702 return;
michael@0 4703 }
michael@0 4704
michael@0 4705 // "If start node is an editable Text node, call deleteData() on it, with
michael@0 4706 // start offset as the first argument and (length of start node − start
michael@0 4707 // offset) as the second argument."
michael@0 4708 if (isEditable(startNode)
michael@0 4709 && startNode.nodeType == Node.TEXT_NODE) {
michael@0 4710 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
michael@0 4711 }
michael@0 4712
michael@0 4713 // "Let node list be a list of nodes, initially empty."
michael@0 4714 //
michael@0 4715 // "For each node contained in the active range, append node to node list
michael@0 4716 // if the last member of node list (if any) is not an ancestor of node;
michael@0 4717 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
michael@0 4718 var nodeList = getContainedNodes(getActiveRange(),
michael@0 4719 function(node) {
michael@0 4720 return isEditable(node)
michael@0 4721 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
michael@0 4722 }
michael@0 4723 );
michael@0 4724
michael@0 4725 // "For each node in node list:"
michael@0 4726 for (var i = 0; i < nodeList.length; i++) {
michael@0 4727 var node = nodeList[i];
michael@0 4728
michael@0 4729 // "Let parent be the parent of node."
michael@0 4730 var parent_ = node.parentNode;
michael@0 4731
michael@0 4732 // "Remove node from parent."
michael@0 4733 parent_.removeChild(node);
michael@0 4734
michael@0 4735 // "If the block node of parent has no visible children, and parent is
michael@0 4736 // editable or an editing host, call createElement("br") on the context
michael@0 4737 // object and append the result as the last child of parent."
michael@0 4738 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
michael@0 4739 && (isEditable(parent_) || isEditingHost(parent_))) {
michael@0 4740 parent_.appendChild(document.createElement("br"));
michael@0 4741 }
michael@0 4742
michael@0 4743 // "If strip wrappers is true or parent is not an ancestor container of
michael@0 4744 // start node, while parent is an editable inline node with length 0,
michael@0 4745 // let grandparent be the parent of parent, then remove parent from
michael@0 4746 // grandparent, then set parent to grandparent."
michael@0 4747 if (stripWrappers
michael@0 4748 || (!isAncestor(parent_, startNode) && parent_ != startNode)) {
michael@0 4749 while (isEditable(parent_)
michael@0 4750 && isInlineNode(parent_)
michael@0 4751 && getNodeLength(parent_) == 0) {
michael@0 4752 var grandparent = parent_.parentNode;
michael@0 4753 grandparent.removeChild(parent_);
michael@0 4754 parent_ = grandparent;
michael@0 4755 }
michael@0 4756 }
michael@0 4757 }
michael@0 4758
michael@0 4759 // "If end node is an editable Text node, call deleteData(0, end offset) on
michael@0 4760 // it."
michael@0 4761 if (isEditable(endNode)
michael@0 4762 && endNode.nodeType == Node.TEXT_NODE) {
michael@0 4763 endNode.deleteData(0, endOffset);
michael@0 4764 }
michael@0 4765
michael@0 4766 // "Canonicalize whitespace at the active range's start, with fix collapsed
michael@0 4767 // space false."
michael@0 4768 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
michael@0 4769
michael@0 4770 // "Canonicalize whitespace at the active range's end, with fix collapsed
michael@0 4771 // space false."
michael@0 4772 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
michael@0 4773
michael@0 4774 // "If block merging is false, or start block or end block is null, or
michael@0 4775 // start block is not in the same editing host as end block, or start block
michael@0 4776 // and end block are the same:"
michael@0 4777 if (!blockMerging
michael@0 4778 || !startBlock
michael@0 4779 || !endBlock
michael@0 4780 || !inSameEditingHost(startBlock, endBlock)
michael@0 4781 || startBlock == endBlock) {
michael@0 4782 // "If direction is "forward", call collapseToStart() on the context
michael@0 4783 // object's Selection."
michael@0 4784 if (direction == "forward") {
michael@0 4785 if (getSelection().rangeCount) {
michael@0 4786 getSelection().collapseToStart();
michael@0 4787 }
michael@0 4788 getActiveRange().collapse(true);
michael@0 4789
michael@0 4790 // "Otherwise, call collapseToEnd() on the context object's Selection."
michael@0 4791 } else {
michael@0 4792 if (getSelection().rangeCount) {
michael@0 4793 getSelection().collapseToEnd();
michael@0 4794 }
michael@0 4795 getActiveRange().collapse(false);
michael@0 4796 }
michael@0 4797
michael@0 4798 // "Restore states and values from overrides."
michael@0 4799 restoreStatesAndValues(overrides);
michael@0 4800
michael@0 4801 // "Abort these steps."
michael@0 4802 return;
michael@0 4803 }
michael@0 4804
michael@0 4805 // "If start block has one child, which is a collapsed block prop, remove
michael@0 4806 // its child from it."
michael@0 4807 if (startBlock.children.length == 1
michael@0 4808 && isCollapsedBlockProp(startBlock.firstChild)) {
michael@0 4809 startBlock.removeChild(startBlock.firstChild);
michael@0 4810 }
michael@0 4811
michael@0 4812 // "If start block is an ancestor of end block:"
michael@0 4813 if (isAncestor(startBlock, endBlock)) {
michael@0 4814 // "Let reference node be end block."
michael@0 4815 var referenceNode = endBlock;
michael@0 4816
michael@0 4817 // "While reference node is not a child of start block, set reference
michael@0 4818 // node to its parent."
michael@0 4819 while (referenceNode.parentNode != startBlock) {
michael@0 4820 referenceNode = referenceNode.parentNode;
michael@0 4821 }
michael@0 4822
michael@0 4823 // "Call collapse() on the context object's Selection, with first
michael@0 4824 // argument start block and second argument the index of reference
michael@0 4825 // node."
michael@0 4826 getSelection().collapse(startBlock, getNodeIndex(referenceNode));
michael@0 4827 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
michael@0 4828 getActiveRange().collapse(true);
michael@0 4829
michael@0 4830 // "If end block has no children:"
michael@0 4831 if (!endBlock.hasChildNodes()) {
michael@0 4832 // "While end block is editable and is the only child of its parent
michael@0 4833 // and is not a child of start block, let parent equal end block,
michael@0 4834 // then remove end block from parent, then set end block to
michael@0 4835 // parent."
michael@0 4836 while (isEditable(endBlock)
michael@0 4837 && endBlock.parentNode.childNodes.length == 1
michael@0 4838 && endBlock.parentNode != startBlock) {
michael@0 4839 var parent_ = endBlock;
michael@0 4840 parent_.removeChild(endBlock);
michael@0 4841 endBlock = parent_;
michael@0 4842 }
michael@0 4843
michael@0 4844 // "If end block is editable and is not an inline node, and its
michael@0 4845 // previousSibling and nextSibling are both inline nodes, call
michael@0 4846 // createElement("br") on the context object and insert it into end
michael@0 4847 // block's parent immediately after end block."
michael@0 4848 if (isEditable(endBlock)
michael@0 4849 && !isInlineNode(endBlock)
michael@0 4850 && isInlineNode(endBlock.previousSibling)
michael@0 4851 && isInlineNode(endBlock.nextSibling)) {
michael@0 4852 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
michael@0 4853 }
michael@0 4854
michael@0 4855 // "If end block is editable, remove it from its parent."
michael@0 4856 if (isEditable(endBlock)) {
michael@0 4857 endBlock.parentNode.removeChild(endBlock);
michael@0 4858 }
michael@0 4859
michael@0 4860 // "Restore states and values from overrides."
michael@0 4861 restoreStatesAndValues(overrides);
michael@0 4862
michael@0 4863 // "Abort these steps."
michael@0 4864 return;
michael@0 4865 }
michael@0 4866
michael@0 4867 // "If end block's firstChild is not an inline node, restore states and
michael@0 4868 // values from overrides, then abort these steps."
michael@0 4869 if (!isInlineNode(endBlock.firstChild)) {
michael@0 4870 restoreStatesAndValues(overrides);
michael@0 4871 return;
michael@0 4872 }
michael@0 4873
michael@0 4874 // "Let children be a list of nodes, initially empty."
michael@0 4875 var children = [];
michael@0 4876
michael@0 4877 // "Append the first child of end block to children."
michael@0 4878 children.push(endBlock.firstChild);
michael@0 4879
michael@0 4880 // "While children's last member is not a br, and children's last
michael@0 4881 // member's nextSibling is an inline node, append children's last
michael@0 4882 // member's nextSibling to children."
michael@0 4883 while (!isHtmlElement(children[children.length - 1], "br")
michael@0 4884 && isInlineNode(children[children.length - 1].nextSibling)) {
michael@0 4885 children.push(children[children.length - 1].nextSibling);
michael@0 4886 }
michael@0 4887
michael@0 4888 // "Record the values of children, and let values be the result."
michael@0 4889 var values = recordValues(children);
michael@0 4890
michael@0 4891 // "While children's first member's parent is not start block, split
michael@0 4892 // the parent of children."
michael@0 4893 while (children[0].parentNode != startBlock) {
michael@0 4894 splitParent(children);
michael@0 4895 }
michael@0 4896
michael@0 4897 // "If children's first member's previousSibling is an editable br,
michael@0 4898 // remove that br from its parent."
michael@0 4899 if (isEditable(children[0].previousSibling)
michael@0 4900 && isHtmlElement(children[0].previousSibling, "br")) {
michael@0 4901 children[0].parentNode.removeChild(children[0].previousSibling);
michael@0 4902 }
michael@0 4903
michael@0 4904 // "Otherwise, if start block is a descendant of end block:"
michael@0 4905 } else if (isDescendant(startBlock, endBlock)) {
michael@0 4906 // "Call collapse() on the context object's Selection, with first
michael@0 4907 // argument start block and second argument start block's length."
michael@0 4908 getSelection().collapse(startBlock, getNodeLength(startBlock));
michael@0 4909 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
michael@0 4910 getActiveRange().collapse(true);
michael@0 4911
michael@0 4912 // "Let reference node be start block."
michael@0 4913 var referenceNode = startBlock;
michael@0 4914
michael@0 4915 // "While reference node is not a child of end block, set reference
michael@0 4916 // node to its parent."
michael@0 4917 while (referenceNode.parentNode != endBlock) {
michael@0 4918 referenceNode = referenceNode.parentNode;
michael@0 4919 }
michael@0 4920
michael@0 4921 // "If reference node's nextSibling is an inline node and start block's
michael@0 4922 // lastChild is a br, remove start block's lastChild from it."
michael@0 4923 if (isInlineNode(referenceNode.nextSibling)
michael@0 4924 && isHtmlElement(startBlock.lastChild, "br")) {
michael@0 4925 startBlock.removeChild(startBlock.lastChild);
michael@0 4926 }
michael@0 4927
michael@0 4928 // "Let nodes to move be a list of nodes, initially empty."
michael@0 4929 var nodesToMove = [];
michael@0 4930
michael@0 4931 // "If reference node's nextSibling is neither null nor a block node,
michael@0 4932 // append it to nodes to move."
michael@0 4933 if (referenceNode.nextSibling
michael@0 4934 && !isBlockNode(referenceNode.nextSibling)) {
michael@0 4935 nodesToMove.push(referenceNode.nextSibling);
michael@0 4936 }
michael@0 4937
michael@0 4938 // "While nodes to move is nonempty and its last member isn't a br and
michael@0 4939 // its last member's nextSibling is neither null nor a block node,
michael@0 4940 // append its last member's nextSibling to nodes to move."
michael@0 4941 if (nodesToMove.length
michael@0 4942 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
michael@0 4943 && nodesToMove[nodesToMove.length - 1].nextSibling
michael@0 4944 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
michael@0 4945 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
michael@0 4946 }
michael@0 4947
michael@0 4948 // "Record the values of nodes to move, and let values be the result."
michael@0 4949 var values = recordValues(nodesToMove);
michael@0 4950
michael@0 4951 // "For each node in nodes to move, append node as the last child of
michael@0 4952 // start block, preserving ranges."
michael@0 4953 nodesToMove.forEach(function(node) {
michael@0 4954 movePreservingRanges(node, startBlock, -1);
michael@0 4955 });
michael@0 4956
michael@0 4957 // "Otherwise:"
michael@0 4958 } else {
michael@0 4959 // "Call collapse() on the context object's Selection, with first
michael@0 4960 // argument start block and second argument start block's length."
michael@0 4961 getSelection().collapse(startBlock, getNodeLength(startBlock));
michael@0 4962 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
michael@0 4963 getActiveRange().collapse(true);
michael@0 4964
michael@0 4965 // "If end block's firstChild is an inline node and start block's
michael@0 4966 // lastChild is a br, remove start block's lastChild from it."
michael@0 4967 if (isInlineNode(endBlock.firstChild)
michael@0 4968 && isHtmlElement(startBlock.lastChild, "br")) {
michael@0 4969 startBlock.removeChild(startBlock.lastChild);
michael@0 4970 }
michael@0 4971
michael@0 4972 // "Record the values of end block's children, and let values be the
michael@0 4973 // result."
michael@0 4974 var values = recordValues([].slice.call(endBlock.childNodes));
michael@0 4975
michael@0 4976 // "While end block has children, append the first child of end block
michael@0 4977 // to start block, preserving ranges."
michael@0 4978 while (endBlock.hasChildNodes()) {
michael@0 4979 movePreservingRanges(endBlock.firstChild, startBlock, -1);
michael@0 4980 }
michael@0 4981
michael@0 4982 // "While end block has no children, let parent be the parent of end
michael@0 4983 // block, then remove end block from parent, then set end block to
michael@0 4984 // parent."
michael@0 4985 while (!endBlock.hasChildNodes()) {
michael@0 4986 var parent_ = endBlock.parentNode;
michael@0 4987 parent_.removeChild(endBlock);
michael@0 4988 endBlock = parent_;
michael@0 4989 }
michael@0 4990 }
michael@0 4991
michael@0 4992 // "Let ancestor be start block."
michael@0 4993 var ancestor = startBlock;
michael@0 4994
michael@0 4995 // "While ancestor has an inclusive ancestor ol in the same editing host
michael@0 4996 // whose nextSibling is also an ol in the same editing host, or an
michael@0 4997 // inclusive ancestor ul in the same editing host whose nextSibling is also
michael@0 4998 // a ul in the same editing host:"
michael@0 4999 while (getInclusiveAncestors(ancestor).some(function(node) {
michael@0 5000 return inSameEditingHost(ancestor, node)
michael@0 5001 && (
michael@0 5002 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol"))
michael@0 5003 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
michael@0 5004 ) && inSameEditingHost(ancestor, node.nextSibling);
michael@0 5005 })) {
michael@0 5006 // "While ancestor and its nextSibling are not both ols in the same
michael@0 5007 // editing host, and are also not both uls in the same editing host,
michael@0 5008 // set ancestor to its parent."
michael@0 5009 while (!(
michael@0 5010 isHtmlElement(ancestor, "ol")
michael@0 5011 && isHtmlElement(ancestor.nextSibling, "ol")
michael@0 5012 && inSameEditingHost(ancestor, ancestor.nextSibling)
michael@0 5013 ) && !(
michael@0 5014 isHtmlElement(ancestor, "ul")
michael@0 5015 && isHtmlElement(ancestor.nextSibling, "ul")
michael@0 5016 && inSameEditingHost(ancestor, ancestor.nextSibling)
michael@0 5017 )) {
michael@0 5018 ancestor = ancestor.parentNode;
michael@0 5019 }
michael@0 5020
michael@0 5021 // "While ancestor's nextSibling has children, append ancestor's
michael@0 5022 // nextSibling's firstChild as the last child of ancestor, preserving
michael@0 5023 // ranges."
michael@0 5024 while (ancestor.nextSibling.hasChildNodes()) {
michael@0 5025 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
michael@0 5026 }
michael@0 5027
michael@0 5028 // "Remove ancestor's nextSibling from its parent."
michael@0 5029 ancestor.parentNode.removeChild(ancestor.nextSibling);
michael@0 5030 }
michael@0 5031
michael@0 5032 // "Restore the values from values."
michael@0 5033 restoreValues(values);
michael@0 5034
michael@0 5035 // "If start block has no children, call createElement("br") on the context
michael@0 5036 // object and append the result as the last child of start block."
michael@0 5037 if (!startBlock.hasChildNodes()) {
michael@0 5038 startBlock.appendChild(document.createElement("br"));
michael@0 5039 }
michael@0 5040
michael@0 5041 // "Remove extraneous line breaks at the end of start block."
michael@0 5042 removeExtraneousLineBreaksAtTheEndOf(startBlock);
michael@0 5043
michael@0 5044 // "Restore states and values from overrides."
michael@0 5045 restoreStatesAndValues(overrides);
michael@0 5046 }
michael@0 5047
michael@0 5048
michael@0 5049 //@}
michael@0 5050 ///// Splitting a node list's parent /////
michael@0 5051 //@{
michael@0 5052
michael@0 5053 function splitParent(nodeList) {
michael@0 5054 // "Let original parent be the parent of the first member of node list."
michael@0 5055 var originalParent = nodeList[0].parentNode;
michael@0 5056
michael@0 5057 // "If original parent is not editable or its parent is null, do nothing
michael@0 5058 // and abort these steps."
michael@0 5059 if (!isEditable(originalParent)
michael@0 5060 || !originalParent.parentNode) {
michael@0 5061 return;
michael@0 5062 }
michael@0 5063
michael@0 5064 // "If the first child of original parent is in node list, remove
michael@0 5065 // extraneous line breaks before original parent."
michael@0 5066 if (nodeList.indexOf(originalParent.firstChild) != -1) {
michael@0 5067 removeExtraneousLineBreaksBefore(originalParent);
michael@0 5068 }
michael@0 5069
michael@0 5070 // "If the first child of original parent is in node list, and original
michael@0 5071 // parent follows a line break, set follows line break to true. Otherwise,
michael@0 5072 // set follows line break to false."
michael@0 5073 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
michael@0 5074 && followsLineBreak(originalParent);
michael@0 5075
michael@0 5076 // "If the last child of original parent is in node list, and original
michael@0 5077 // parent precedes a line break, set precedes line break to true.
michael@0 5078 // Otherwise, set precedes line break to false."
michael@0 5079 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
michael@0 5080 && precedesLineBreak(originalParent);
michael@0 5081
michael@0 5082 // "If the first child of original parent is not in node list, but its last
michael@0 5083 // child is:"
michael@0 5084 if (nodeList.indexOf(originalParent.firstChild) == -1
michael@0 5085 && nodeList.indexOf(originalParent.lastChild) != -1) {
michael@0 5086 // "For each node in node list, in reverse order, insert node into the
michael@0 5087 // parent of original parent immediately after original parent,
michael@0 5088 // preserving ranges."
michael@0 5089 for (var i = nodeList.length - 1; i >= 0; i--) {
michael@0 5090 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent));
michael@0 5091 }
michael@0 5092
michael@0 5093 // "If precedes line break is true, and the last member of node list
michael@0 5094 // does not precede a line break, call createElement("br") on the
michael@0 5095 // context object and insert the result immediately after the last
michael@0 5096 // member of node list."
michael@0 5097 if (precedesLineBreak_
michael@0 5098 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
michael@0 5099 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
michael@0 5100 }
michael@0 5101
michael@0 5102 // "Remove extraneous line breaks at the end of original parent."
michael@0 5103 removeExtraneousLineBreaksAtTheEndOf(originalParent);
michael@0 5104
michael@0 5105 // "Abort these steps."
michael@0 5106 return;
michael@0 5107 }
michael@0 5108
michael@0 5109 // "If the first child of original parent is not in node list:"
michael@0 5110 if (nodeList.indexOf(originalParent.firstChild) == -1) {
michael@0 5111 // "Let cloned parent be the result of calling cloneNode(false) on
michael@0 5112 // original parent."
michael@0 5113 var clonedParent = originalParent.cloneNode(false);
michael@0 5114
michael@0 5115 // "If original parent has an id attribute, unset it."
michael@0 5116 originalParent.removeAttribute("id");
michael@0 5117
michael@0 5118 // "Insert cloned parent into the parent of original parent immediately
michael@0 5119 // before original parent."
michael@0 5120 originalParent.parentNode.insertBefore(clonedParent, originalParent);
michael@0 5121
michael@0 5122 // "While the previousSibling of the first member of node list is not
michael@0 5123 // null, append the first child of original parent as the last child of
michael@0 5124 // cloned parent, preserving ranges."
michael@0 5125 while (nodeList[0].previousSibling) {
michael@0 5126 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length);
michael@0 5127 }
michael@0 5128 }
michael@0 5129
michael@0 5130 // "For each node in node list, insert node into the parent of original
michael@0 5131 // parent immediately before original parent, preserving ranges."
michael@0 5132 for (var i = 0; i < nodeList.length; i++) {
michael@0 5133 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent));
michael@0 5134 }
michael@0 5135
michael@0 5136 // "If follows line break is true, and the first member of node list does
michael@0 5137 // not follow a line break, call createElement("br") on the context object
michael@0 5138 // and insert the result immediately before the first member of node list."
michael@0 5139 if (followsLineBreak_
michael@0 5140 && !followsLineBreak(nodeList[0])) {
michael@0 5141 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
michael@0 5142 }
michael@0 5143
michael@0 5144 // "If the last member of node list is an inline node other than a br, and
michael@0 5145 // the first child of original parent is a br, and original parent is not
michael@0 5146 // an inline node, remove the first child of original parent from original
michael@0 5147 // parent."
michael@0 5148 if (isInlineNode(nodeList[nodeList.length - 1])
michael@0 5149 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
michael@0 5150 && isHtmlElement(originalParent.firstChild, "br")
michael@0 5151 && !isInlineNode(originalParent)) {
michael@0 5152 originalParent.removeChild(originalParent.firstChild);
michael@0 5153 }
michael@0 5154
michael@0 5155 // "If original parent has no children:"
michael@0 5156 if (!originalParent.hasChildNodes()) {
michael@0 5157 // "Remove original parent from its parent."
michael@0 5158 originalParent.parentNode.removeChild(originalParent);
michael@0 5159
michael@0 5160 // "If precedes line break is true, and the last member of node list
michael@0 5161 // does not precede a line break, call createElement("br") on the
michael@0 5162 // context object and insert the result immediately after the last
michael@0 5163 // member of node list."
michael@0 5164 if (precedesLineBreak_
michael@0 5165 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
michael@0 5166 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
michael@0 5167 }
michael@0 5168
michael@0 5169 // "Otherwise, remove extraneous line breaks before original parent."
michael@0 5170 } else {
michael@0 5171 removeExtraneousLineBreaksBefore(originalParent);
michael@0 5172 }
michael@0 5173
michael@0 5174 // "If node list's last member's nextSibling is null, but its parent is not
michael@0 5175 // null, remove extraneous line breaks at the end of node list's last
michael@0 5176 // member's parent."
michael@0 5177 if (!nodeList[nodeList.length - 1].nextSibling
michael@0 5178 && nodeList[nodeList.length - 1].parentNode) {
michael@0 5179 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
michael@0 5180 }
michael@0 5181 }
michael@0 5182
michael@0 5183 // "To remove a node node while preserving its descendants, split the parent of
michael@0 5184 // node's children if it has any. If it has no children, instead remove it from
michael@0 5185 // its parent."
michael@0 5186 function removePreservingDescendants(node) {
michael@0 5187 if (node.hasChildNodes()) {
michael@0 5188 splitParent([].slice.call(node.childNodes));
michael@0 5189 } else {
michael@0 5190 node.parentNode.removeChild(node);
michael@0 5191 }
michael@0 5192 }
michael@0 5193
michael@0 5194
michael@0 5195 //@}
michael@0 5196 ///// Canonical space sequences /////
michael@0 5197 //@{
michael@0 5198
michael@0 5199 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
michael@0 5200 // "If n is zero, return the empty string."
michael@0 5201 if (n == 0) {
michael@0 5202 return "";
michael@0 5203 }
michael@0 5204
michael@0 5205 // "If n is one and both non-breaking start and non-breaking end are false,
michael@0 5206 // return a single space (U+0020)."
michael@0 5207 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
michael@0 5208 return " ";
michael@0 5209 }
michael@0 5210
michael@0 5211 // "If n is one, return a single non-breaking space (U+00A0)."
michael@0 5212 if (n == 1) {
michael@0 5213 return "\xa0";
michael@0 5214 }
michael@0 5215
michael@0 5216 // "Let buffer be the empty string."
michael@0 5217 var buffer = "";
michael@0 5218
michael@0 5219 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
michael@0 5220 // Otherwise, let it be U+0020 U+00A0."
michael@0 5221 var repeatedPair;
michael@0 5222 if (nonBreakingStart) {
michael@0 5223 repeatedPair = "\xa0 ";
michael@0 5224 } else {
michael@0 5225 repeatedPair = " \xa0";
michael@0 5226 }
michael@0 5227
michael@0 5228 // "While n is greater than three, append repeated pair to buffer and
michael@0 5229 // subtract two from n."
michael@0 5230 while (n > 3) {
michael@0 5231 buffer += repeatedPair;
michael@0 5232 n -= 2;
michael@0 5233 }
michael@0 5234
michael@0 5235 // "If n is three, append a three-element string to buffer depending on
michael@0 5236 // non-breaking start and non-breaking end:"
michael@0 5237 if (n == 3) {
michael@0 5238 buffer +=
michael@0 5239 !nonBreakingStart && !nonBreakingEnd ? " \xa0 "
michael@0 5240 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
michael@0 5241 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
michael@0 5242 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
michael@0 5243 : "impossible";
michael@0 5244
michael@0 5245 // "Otherwise, append a two-element string to buffer depending on
michael@0 5246 // non-breaking start and non-breaking end:"
michael@0 5247 } else {
michael@0 5248 buffer +=
michael@0 5249 !nonBreakingStart && !nonBreakingEnd ? "\xa0 "
michael@0 5250 : nonBreakingStart && !nonBreakingEnd ? "\xa0 "
michael@0 5251 : !nonBreakingStart && nonBreakingEnd ? " \xa0"
michael@0 5252 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
michael@0 5253 : "impossible";
michael@0 5254 }
michael@0 5255
michael@0 5256 // "Return buffer."
michael@0 5257 return buffer;
michael@0 5258 }
michael@0 5259
michael@0 5260 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
michael@0 5261 if (fixCollapsedSpace === undefined) {
michael@0 5262 // "an optional boolean argument fix collapsed space that defaults to
michael@0 5263 // true"
michael@0 5264 fixCollapsedSpace = true;
michael@0 5265 }
michael@0 5266
michael@0 5267 // "If node is neither editable nor an editing host, abort these steps."
michael@0 5268 if (!isEditable(node) && !isEditingHost(node)) {
michael@0 5269 return;
michael@0 5270 }
michael@0 5271
michael@0 5272 // "Let start node equal node and let start offset equal offset."
michael@0 5273 var startNode = node;
michael@0 5274 var startOffset = offset;
michael@0 5275
michael@0 5276 // "Repeat the following steps:"
michael@0 5277 while (true) {
michael@0 5278 // "If start node has a child in the same editing host with index start
michael@0 5279 // offset minus one, set start node to that child, then set start
michael@0 5280 // offset to start node's length."
michael@0 5281 if (0 <= startOffset - 1
michael@0 5282 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
michael@0 5283 startNode = startNode.childNodes[startOffset - 1];
michael@0 5284 startOffset = getNodeLength(startNode);
michael@0 5285
michael@0 5286 // "Otherwise, if start offset is zero and start node does not follow a
michael@0 5287 // line break and start node's parent is in the same editing host, set
michael@0 5288 // start offset to start node's index, then set start node to its
michael@0 5289 // parent."
michael@0 5290 } else if (startOffset == 0
michael@0 5291 && !followsLineBreak(startNode)
michael@0 5292 && inSameEditingHost(startNode, startNode.parentNode)) {
michael@0 5293 startOffset = getNodeIndex(startNode);
michael@0 5294 startNode = startNode.parentNode;
michael@0 5295
michael@0 5296 // "Otherwise, if start node is a Text node and its parent's resolved
michael@0 5297 // value for "white-space" is neither "pre" nor "pre-wrap" and start
michael@0 5298 // offset is not zero and the (start offset − 1)st element of start
michael@0 5299 // node's data is a space (0x0020) or non-breaking space (0x00A0),
michael@0 5300 // subtract one from start offset."
michael@0 5301 } else if (startNode.nodeType == Node.TEXT_NODE
michael@0 5302 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
michael@0 5303 && startOffset != 0
michael@0 5304 && /[ \xa0]/.test(startNode.data[startOffset - 1])) {
michael@0 5305 startOffset--;
michael@0 5306
michael@0 5307 // "Otherwise, break from this loop."
michael@0 5308 } else {
michael@0 5309 break;
michael@0 5310 }
michael@0 5311 }
michael@0 5312
michael@0 5313 // "Let end node equal start node and end offset equal start offset."
michael@0 5314 var endNode = startNode;
michael@0 5315 var endOffset = startOffset;
michael@0 5316
michael@0 5317 // "Let length equal zero."
michael@0 5318 var length = 0;
michael@0 5319
michael@0 5320 // "Let collapse spaces be true if start offset is zero and start node
michael@0 5321 // follows a line break, otherwise false."
michael@0 5322 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
michael@0 5323
michael@0 5324 // "Repeat the following steps:"
michael@0 5325 while (true) {
michael@0 5326 // "If end node has a child in the same editing host with index end
michael@0 5327 // offset, set end node to that child, then set end offset to zero."
michael@0 5328 if (endOffset < endNode.childNodes.length
michael@0 5329 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
michael@0 5330 endNode = endNode.childNodes[endOffset];
michael@0 5331 endOffset = 0;
michael@0 5332
michael@0 5333 // "Otherwise, if end offset is end node's length and end node does not
michael@0 5334 // precede a line break and end node's parent is in the same editing
michael@0 5335 // host, set end offset to one plus end node's index, then set end node
michael@0 5336 // to its parent."
michael@0 5337 } else if (endOffset == getNodeLength(endNode)
michael@0 5338 && !precedesLineBreak(endNode)
michael@0 5339 && inSameEditingHost(endNode, endNode.parentNode)) {
michael@0 5340 endOffset = 1 + getNodeIndex(endNode);
michael@0 5341 endNode = endNode.parentNode;
michael@0 5342
michael@0 5343 // "Otherwise, if end node is a Text node and its parent's resolved
michael@0 5344 // value for "white-space" is neither "pre" nor "pre-wrap" and end
michael@0 5345 // offset is not end node's length and the end offsetth element of
michael@0 5346 // end node's data is a space (0x0020) or non-breaking space (0x00A0):"
michael@0 5347 } else if (endNode.nodeType == Node.TEXT_NODE
michael@0 5348 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
michael@0 5349 && endOffset != getNodeLength(endNode)
michael@0 5350 && /[ \xa0]/.test(endNode.data[endOffset])) {
michael@0 5351 // "If fix collapsed space is true, and collapse spaces is true,
michael@0 5352 // and the end offsetth code unit of end node's data is a space
michael@0 5353 // (0x0020): call deleteData(end offset, 1) on end node, then
michael@0 5354 // continue this loop from the beginning."
michael@0 5355 if (fixCollapsedSpace
michael@0 5356 && collapseSpaces
michael@0 5357 && " " == endNode.data[endOffset]) {
michael@0 5358 endNode.deleteData(endOffset, 1);
michael@0 5359 continue;
michael@0 5360 }
michael@0 5361
michael@0 5362 // "Set collapse spaces to true if the end offsetth element of end
michael@0 5363 // node's data is a space (0x0020), false otherwise."
michael@0 5364 collapseSpaces = " " == endNode.data[endOffset];
michael@0 5365
michael@0 5366 // "Add one to end offset."
michael@0 5367 endOffset++;
michael@0 5368
michael@0 5369 // "Add one to length."
michael@0 5370 length++;
michael@0 5371
michael@0 5372 // "Otherwise, break from this loop."
michael@0 5373 } else {
michael@0 5374 break;
michael@0 5375 }
michael@0 5376 }
michael@0 5377
michael@0 5378 // "If fix collapsed space is true, then while (start node, start offset)
michael@0 5379 // is before (end node, end offset):"
michael@0 5380 if (fixCollapsedSpace) {
michael@0 5381 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
michael@0 5382 // "If end node has a child in the same editing host with index end
michael@0 5383 // offset − 1, set end node to that child, then set end offset to end
michael@0 5384 // node's length."
michael@0 5385 if (0 <= endOffset - 1
michael@0 5386 && endOffset - 1 < endNode.childNodes.length
michael@0 5387 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
michael@0 5388 endNode = endNode.childNodes[endOffset - 1];
michael@0 5389 endOffset = getNodeLength(endNode);
michael@0 5390
michael@0 5391 // "Otherwise, if end offset is zero and end node's parent is in the
michael@0 5392 // same editing host, set end offset to end node's index, then set end
michael@0 5393 // node to its parent."
michael@0 5394 } else if (endOffset == 0
michael@0 5395 && inSameEditingHost(endNode, endNode.parentNode)) {
michael@0 5396 endOffset = getNodeIndex(endNode);
michael@0 5397 endNode = endNode.parentNode;
michael@0 5398
michael@0 5399 // "Otherwise, if end node is a Text node and its parent's resolved
michael@0 5400 // value for "white-space" is neither "pre" nor "pre-wrap" and end
michael@0 5401 // offset is end node's length and the last code unit of end node's
michael@0 5402 // data is a space (0x0020) and end node precedes a line break:"
michael@0 5403 } else if (endNode.nodeType == Node.TEXT_NODE
michael@0 5404 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
michael@0 5405 && endOffset == getNodeLength(endNode)
michael@0 5406 && endNode.data[endNode.data.length - 1] == " "
michael@0 5407 && precedesLineBreak(endNode)) {
michael@0 5408 // "Subtract one from end offset."
michael@0 5409 endOffset--;
michael@0 5410
michael@0 5411 // "Subtract one from length."
michael@0 5412 length--;
michael@0 5413
michael@0 5414 // "Call deleteData(end offset, 1) on end node."
michael@0 5415 endNode.deleteData(endOffset, 1);
michael@0 5416
michael@0 5417 // "Otherwise, break from this loop."
michael@0 5418 } else {
michael@0 5419 break;
michael@0 5420 }
michael@0 5421 }
michael@0 5422 }
michael@0 5423
michael@0 5424 // "Let replacement whitespace be the canonical space sequence of length
michael@0 5425 // length. non-breaking start is true if start offset is zero and start
michael@0 5426 // node follows a line break, and false otherwise. non-breaking end is true
michael@0 5427 // if end offset is end node's length and end node precedes a line break,
michael@0 5428 // and false otherwise."
michael@0 5429 var replacementWhitespace = canonicalSpaceSequence(length,
michael@0 5430 startOffset == 0 && followsLineBreak(startNode),
michael@0 5431 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
michael@0 5432
michael@0 5433 // "While (start node, start offset) is before (end node, end offset):"
michael@0 5434 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
michael@0 5435 // "If start node has a child with index start offset, set start node
michael@0 5436 // to that child, then set start offset to zero."
michael@0 5437 if (startOffset < startNode.childNodes.length) {
michael@0 5438 startNode = startNode.childNodes[startOffset];
michael@0 5439 startOffset = 0;
michael@0 5440
michael@0 5441 // "Otherwise, if start node is not a Text node or if start offset is
michael@0 5442 // start node's length, set start offset to one plus start node's
michael@0 5443 // index, then set start node to its parent."
michael@0 5444 } else if (startNode.nodeType != Node.TEXT_NODE
michael@0 5445 || startOffset == getNodeLength(startNode)) {
michael@0 5446 startOffset = 1 + getNodeIndex(startNode);
michael@0 5447 startNode = startNode.parentNode;
michael@0 5448
michael@0 5449 // "Otherwise:"
michael@0 5450 } else {
michael@0 5451 // "Remove the first element from replacement whitespace, and let
michael@0 5452 // element be that element."
michael@0 5453 var element = replacementWhitespace[0];
michael@0 5454 replacementWhitespace = replacementWhitespace.slice(1);
michael@0 5455
michael@0 5456 // "If element is not the same as the start offsetth element of
michael@0 5457 // start node's data:"
michael@0 5458 if (element != startNode.data[startOffset]) {
michael@0 5459 // "Call insertData(start offset, element) on start node."
michael@0 5460 startNode.insertData(startOffset, element);
michael@0 5461
michael@0 5462 // "Call deleteData(start offset + 1, 1) on start node."
michael@0 5463 startNode.deleteData(startOffset + 1, 1);
michael@0 5464 }
michael@0 5465
michael@0 5466 // "Add one to start offset."
michael@0 5467 startOffset++;
michael@0 5468 }
michael@0 5469 }
michael@0 5470 }
michael@0 5471
michael@0 5472
michael@0 5473 //@}
michael@0 5474 ///// Indenting and outdenting /////
michael@0 5475 //@{
michael@0 5476
michael@0 5477 function indentNodes(nodeList) {
michael@0 5478 // "If node list is empty, do nothing and abort these steps."
michael@0 5479 if (!nodeList.length) {
michael@0 5480 return;
michael@0 5481 }
michael@0 5482
michael@0 5483 // "Let first node be the first member of node list."
michael@0 5484 var firstNode = nodeList[0];
michael@0 5485
michael@0 5486 // "If first node's parent is an ol or ul:"
michael@0 5487 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
michael@0 5488 // "Let tag be the local name of the parent of first node."
michael@0 5489 var tag = firstNode.parentNode.tagName;
michael@0 5490
michael@0 5491 // "Wrap node list, with sibling criteria returning true for an HTML
michael@0 5492 // element with local name tag and false otherwise, and new parent
michael@0 5493 // instructions returning the result of calling createElement(tag) on
michael@0 5494 // the ownerDocument of first node."
michael@0 5495 wrap(nodeList,
michael@0 5496 function(node) { return isHtmlElement(node, tag) },
michael@0 5497 function() { return firstNode.ownerDocument.createElement(tag) });
michael@0 5498
michael@0 5499 // "Abort these steps."
michael@0 5500 return;
michael@0 5501 }
michael@0 5502
michael@0 5503 // "Wrap node list, with sibling criteria returning true for a simple
michael@0 5504 // indentation element and false otherwise, and new parent instructions
michael@0 5505 // returning the result of calling createElement("blockquote") on the
michael@0 5506 // ownerDocument of first node. Let new parent be the result."
michael@0 5507 var newParent = wrap(nodeList,
michael@0 5508 function(node) { return isSimpleIndentationElement(node) },
michael@0 5509 function() { return firstNode.ownerDocument.createElement("blockquote") });
michael@0 5510
michael@0 5511 // "Fix disallowed ancestors of new parent."
michael@0 5512 fixDisallowedAncestors(newParent);
michael@0 5513 }
michael@0 5514
michael@0 5515 function outdentNode(node) {
michael@0 5516 // "If node is not editable, abort these steps."
michael@0 5517 if (!isEditable(node)) {
michael@0 5518 return;
michael@0 5519 }
michael@0 5520
michael@0 5521 // "If node is a simple indentation element, remove node, preserving its
michael@0 5522 // descendants. Then abort these steps."
michael@0 5523 if (isSimpleIndentationElement(node)) {
michael@0 5524 removePreservingDescendants(node);
michael@0 5525 return;
michael@0 5526 }
michael@0 5527
michael@0 5528 // "If node is an indentation element:"
michael@0 5529 if (isIndentationElement(node)) {
michael@0 5530 // "Unset the dir attribute of node, if any."
michael@0 5531 node.removeAttribute("dir");
michael@0 5532
michael@0 5533 // "Unset the margin, padding, and border CSS properties of node."
michael@0 5534 node.style.margin = "";
michael@0 5535 node.style.padding = "";
michael@0 5536 node.style.border = "";
michael@0 5537 if (node.getAttribute("style") == ""
michael@0 5538 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
michael@0 5539 || node.getAttribute("style") == "border-width: initial; border-color: initial; ") {
michael@0 5540 node.removeAttribute("style");
michael@0 5541 }
michael@0 5542
michael@0 5543 // "Set the tag name of node to "div"."
michael@0 5544 setTagName(node, "div");
michael@0 5545
michael@0 5546 // "Abort these steps."
michael@0 5547 return;
michael@0 5548 }
michael@0 5549
michael@0 5550 // "Let current ancestor be node's parent."
michael@0 5551 var currentAncestor = node.parentNode;
michael@0 5552
michael@0 5553 // "Let ancestor list be a list of nodes, initially empty."
michael@0 5554 var ancestorList = [];
michael@0 5555
michael@0 5556 // "While current ancestor is an editable Element that is neither a simple
michael@0 5557 // indentation element nor an ol nor a ul, append current ancestor to
michael@0 5558 // ancestor list and then set current ancestor to its parent."
michael@0 5559 while (isEditable(currentAncestor)
michael@0 5560 && currentAncestor.nodeType == Node.ELEMENT_NODE
michael@0 5561 && !isSimpleIndentationElement(currentAncestor)
michael@0 5562 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
michael@0 5563 ancestorList.push(currentAncestor);
michael@0 5564 currentAncestor = currentAncestor.parentNode;
michael@0 5565 }
michael@0 5566
michael@0 5567 // "If current ancestor is not an editable simple indentation element:"
michael@0 5568 if (!isEditable(currentAncestor)
michael@0 5569 || !isSimpleIndentationElement(currentAncestor)) {
michael@0 5570 // "Let current ancestor be node's parent."
michael@0 5571 currentAncestor = node.parentNode;
michael@0 5572
michael@0 5573 // "Let ancestor list be the empty list."
michael@0 5574 ancestorList = [];
michael@0 5575
michael@0 5576 // "While current ancestor is an editable Element that is neither an
michael@0 5577 // indentation element nor an ol nor a ul, append current ancestor to
michael@0 5578 // ancestor list and then set current ancestor to its parent."
michael@0 5579 while (isEditable(currentAncestor)
michael@0 5580 && currentAncestor.nodeType == Node.ELEMENT_NODE
michael@0 5581 && !isIndentationElement(currentAncestor)
michael@0 5582 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
michael@0 5583 ancestorList.push(currentAncestor);
michael@0 5584 currentAncestor = currentAncestor.parentNode;
michael@0 5585 }
michael@0 5586 }
michael@0 5587
michael@0 5588 // "If node is an ol or ul and current ancestor is not an editable
michael@0 5589 // indentation element:"
michael@0 5590 if (isHtmlElement(node, ["OL", "UL"])
michael@0 5591 && (!isEditable(currentAncestor)
michael@0 5592 || !isIndentationElement(currentAncestor))) {
michael@0 5593 // "Unset the reversed, start, and type attributes of node, if any are
michael@0 5594 // set."
michael@0 5595 node.removeAttribute("reversed");
michael@0 5596 node.removeAttribute("start");
michael@0 5597 node.removeAttribute("type");
michael@0 5598
michael@0 5599 // "Let children be the children of node."
michael@0 5600 var children = [].slice.call(node.childNodes);
michael@0 5601
michael@0 5602 // "If node has attributes, and its parent is not an ol or ul, set the
michael@0 5603 // tag name of node to "div"."
michael@0 5604 if (node.attributes.length
michael@0 5605 && !isHtmlElement(node.parentNode, ["OL", "UL"])) {
michael@0 5606 setTagName(node, "div");
michael@0 5607
michael@0 5608 // "Otherwise:"
michael@0 5609 } else {
michael@0 5610 // "Record the values of node's children, and let values be the
michael@0 5611 // result."
michael@0 5612 var values = recordValues([].slice.call(node.childNodes));
michael@0 5613
michael@0 5614 // "Remove node, preserving its descendants."
michael@0 5615 removePreservingDescendants(node);
michael@0 5616
michael@0 5617 // "Restore the values from values."
michael@0 5618 restoreValues(values);
michael@0 5619 }
michael@0 5620
michael@0 5621 // "Fix disallowed ancestors of each member of children."
michael@0 5622 for (var i = 0; i < children.length; i++) {
michael@0 5623 fixDisallowedAncestors(children[i]);
michael@0 5624 }
michael@0 5625
michael@0 5626 // "Abort these steps."
michael@0 5627 return;
michael@0 5628 }
michael@0 5629
michael@0 5630 // "If current ancestor is not an editable indentation element, abort these
michael@0 5631 // steps."
michael@0 5632 if (!isEditable(currentAncestor)
michael@0 5633 || !isIndentationElement(currentAncestor)) {
michael@0 5634 return;
michael@0 5635 }
michael@0 5636
michael@0 5637 // "Append current ancestor to ancestor list."
michael@0 5638 ancestorList.push(currentAncestor);
michael@0 5639
michael@0 5640 // "Let original ancestor be current ancestor."
michael@0 5641 var originalAncestor = currentAncestor;
michael@0 5642
michael@0 5643 // "While ancestor list is not empty:"
michael@0 5644 while (ancestorList.length) {
michael@0 5645 // "Let current ancestor be the last member of ancestor list."
michael@0 5646 //
michael@0 5647 // "Remove the last member of ancestor list."
michael@0 5648 currentAncestor = ancestorList.pop();
michael@0 5649
michael@0 5650 // "Let target be the child of current ancestor that is equal to either
michael@0 5651 // node or the last member of ancestor list."
michael@0 5652 var target = node.parentNode == currentAncestor
michael@0 5653 ? node
michael@0 5654 : ancestorList[ancestorList.length - 1];
michael@0 5655
michael@0 5656 // "If target is an inline node that is not a br, and its nextSibling
michael@0 5657 // is a br, remove target's nextSibling from its parent."
michael@0 5658 if (isInlineNode(target)
michael@0 5659 && !isHtmlElement(target, "BR")
michael@0 5660 && isHtmlElement(target.nextSibling, "BR")) {
michael@0 5661 target.parentNode.removeChild(target.nextSibling);
michael@0 5662 }
michael@0 5663
michael@0 5664 // "Let preceding siblings be the preceding siblings of target, and let
michael@0 5665 // following siblings be the following siblings of target."
michael@0 5666 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target));
michael@0 5667 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target));
michael@0 5668
michael@0 5669 // "Indent preceding siblings."
michael@0 5670 indentNodes(precedingSiblings);
michael@0 5671
michael@0 5672 // "Indent following siblings."
michael@0 5673 indentNodes(followingSiblings);
michael@0 5674 }
michael@0 5675
michael@0 5676 // "Outdent original ancestor."
michael@0 5677 outdentNode(originalAncestor);
michael@0 5678 }
michael@0 5679
michael@0 5680
michael@0 5681 //@}
michael@0 5682 ///// Toggling lists /////
michael@0 5683 //@{
michael@0 5684
michael@0 5685 function toggleLists(tagName) {
michael@0 5686 // "Let mode be "disable" if the selection's list state is tag name, and
michael@0 5687 // "enable" otherwise."
michael@0 5688 var mode = getSelectionListState() == tagName ? "disable" : "enable";
michael@0 5689
michael@0 5690 var range = getActiveRange();
michael@0 5691 tagName = tagName.toUpperCase();
michael@0 5692
michael@0 5693 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
michael@0 5694 // "ol"."
michael@0 5695 var otherTagName = tagName == "OL" ? "UL" : "OL";
michael@0 5696
michael@0 5697 // "Let items be a list of all lis that are ancestor containers of the
michael@0 5698 // range's start and/or end node."
michael@0 5699 //
michael@0 5700 // It's annoying to get this in tree order using functional stuff without
michael@0 5701 // doing getDescendants(document), which is slow, so I do it imperatively.
michael@0 5702 var items = [];
michael@0 5703 (function(){
michael@0 5704 for (
michael@0 5705 var ancestorContainer = range.endContainer;
michael@0 5706 ancestorContainer != range.commonAncestorContainer;
michael@0 5707 ancestorContainer = ancestorContainer.parentNode
michael@0 5708 ) {
michael@0 5709 if (isHtmlElement(ancestorContainer, "li")) {
michael@0 5710 items.unshift(ancestorContainer);
michael@0 5711 }
michael@0 5712 }
michael@0 5713 for (
michael@0 5714 var ancestorContainer = range.startContainer;
michael@0 5715 ancestorContainer;
michael@0 5716 ancestorContainer = ancestorContainer.parentNode
michael@0 5717 ) {
michael@0 5718 if (isHtmlElement(ancestorContainer, "li")) {
michael@0 5719 items.unshift(ancestorContainer);
michael@0 5720 }
michael@0 5721 }
michael@0 5722 })();
michael@0 5723
michael@0 5724 // "For each item in items, normalize sublists of item."
michael@0 5725 items.forEach(normalizeSublists);
michael@0 5726
michael@0 5727 // "Block-extend the range, and let new range be the result."
michael@0 5728 var newRange = blockExtend(range);
michael@0 5729
michael@0 5730 // "If mode is "enable", then let lists to convert consist of every
michael@0 5731 // editable HTML element with local name other tag name that is contained
michael@0 5732 // in new range, and for every list in lists to convert:"
michael@0 5733 if (mode == "enable") {
michael@0 5734 getAllContainedNodes(newRange, function(node) {
michael@0 5735 return isEditable(node)
michael@0 5736 && isHtmlElement(node, otherTagName);
michael@0 5737 }).forEach(function(list) {
michael@0 5738 // "If list's previousSibling or nextSibling is an editable HTML
michael@0 5739 // element with local name tag name:"
michael@0 5740 if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName))
michael@0 5741 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
michael@0 5742 // "Let children be list's children."
michael@0 5743 var children = [].slice.call(list.childNodes);
michael@0 5744
michael@0 5745 // "Record the values of children, and let values be the
michael@0 5746 // result."
michael@0 5747 var values = recordValues(children);
michael@0 5748
michael@0 5749 // "Split the parent of children."
michael@0 5750 splitParent(children);
michael@0 5751
michael@0 5752 // "Wrap children, with sibling criteria returning true for an
michael@0 5753 // HTML element with local name tag name and false otherwise."
michael@0 5754 wrap(children, function(node) { return isHtmlElement(node, tagName) });
michael@0 5755
michael@0 5756 // "Restore the values from values."
michael@0 5757 restoreValues(values);
michael@0 5758
michael@0 5759 // "Otherwise, set the tag name of list to tag name."
michael@0 5760 } else {
michael@0 5761 setTagName(list, tagName);
michael@0 5762 }
michael@0 5763 });
michael@0 5764 }
michael@0 5765
michael@0 5766 // "Let node list be a list of nodes, initially empty."
michael@0 5767 //
michael@0 5768 // "For each node node contained in new range, if node is editable; the
michael@0 5769 // last member of node list (if any) is not an ancestor of node; node
michael@0 5770 // is not an indentation element; and either node is an ol or ul, or its
michael@0 5771 // parent is an ol or ul, or it is an allowed child of "li"; then append
michael@0 5772 // node to node list."
michael@0 5773 var nodeList = getContainedNodes(newRange, function(node) {
michael@0 5774 return isEditable(node)
michael@0 5775 && !isIndentationElement(node)
michael@0 5776 && (isHtmlElement(node, ["OL", "UL"])
michael@0 5777 || isHtmlElement(node.parentNode, ["OL", "UL"])
michael@0 5778 || isAllowedChild(node, "li"));
michael@0 5779 });
michael@0 5780
michael@0 5781 // "If mode is "enable", remove from node list any ol or ul whose parent is
michael@0 5782 // not also an ol or ul."
michael@0 5783 if (mode == "enable") {
michael@0 5784 nodeList = nodeList.filter(function(node) {
michael@0 5785 return !isHtmlElement(node, ["ol", "ul"])
michael@0 5786 || isHtmlElement(node.parentNode, ["ol", "ul"]);
michael@0 5787 });
michael@0 5788 }
michael@0 5789
michael@0 5790 // "If mode is "disable", then while node list is not empty:"
michael@0 5791 if (mode == "disable") {
michael@0 5792 while (nodeList.length) {
michael@0 5793 // "Let sublist be an empty list of nodes."
michael@0 5794 var sublist = [];
michael@0 5795
michael@0 5796 // "Remove the first member from node list and append it to
michael@0 5797 // sublist."
michael@0 5798 sublist.push(nodeList.shift());
michael@0 5799
michael@0 5800 // "If the first member of sublist is an HTML element with local
michael@0 5801 // name tag name, outdent it and continue this loop from the
michael@0 5802 // beginning."
michael@0 5803 if (isHtmlElement(sublist[0], tagName)) {
michael@0 5804 outdentNode(sublist[0]);
michael@0 5805 continue;
michael@0 5806 }
michael@0 5807
michael@0 5808 // "While node list is not empty, and the first member of node list
michael@0 5809 // is the nextSibling of the last member of sublist and is not an
michael@0 5810 // HTML element with local name tag name, remove the first member
michael@0 5811 // from node list and append it to sublist."
michael@0 5812 while (nodeList.length
michael@0 5813 && nodeList[0] == sublist[sublist.length - 1].nextSibling
michael@0 5814 && !isHtmlElement(nodeList[0], tagName)) {
michael@0 5815 sublist.push(nodeList.shift());
michael@0 5816 }
michael@0 5817
michael@0 5818 // "Record the values of sublist, and let values be the result."
michael@0 5819 var values = recordValues(sublist);
michael@0 5820
michael@0 5821 // "Split the parent of sublist."
michael@0 5822 splitParent(sublist);
michael@0 5823
michael@0 5824 // "Fix disallowed ancestors of each member of sublist."
michael@0 5825 for (var i = 0; i < sublist.length; i++) {
michael@0 5826 fixDisallowedAncestors(sublist[i]);
michael@0 5827 }
michael@0 5828
michael@0 5829 // "Restore the values from values."
michael@0 5830 restoreValues(values);
michael@0 5831 }
michael@0 5832
michael@0 5833 // "Otherwise, while node list is not empty:"
michael@0 5834 } else {
michael@0 5835 while (nodeList.length) {
michael@0 5836 // "Let sublist be an empty list of nodes."
michael@0 5837 var sublist = [];
michael@0 5838
michael@0 5839 // "While either sublist is empty, or node list is not empty and
michael@0 5840 // its first member is the nextSibling of sublist's last member:"
michael@0 5841 while (!sublist.length
michael@0 5842 || (nodeList.length
michael@0 5843 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
michael@0 5844 // "If node list's first member is a p or div, set the tag name
michael@0 5845 // of node list's first member to "li", and append the result
michael@0 5846 // to sublist. Remove the first member from node list."
michael@0 5847 if (isHtmlElement(nodeList[0], ["p", "div"])) {
michael@0 5848 sublist.push(setTagName(nodeList[0], "li"));
michael@0 5849 nodeList.shift();
michael@0 5850
michael@0 5851 // "Otherwise, if the first member of node list is an li or ol
michael@0 5852 // or ul, remove it from node list and append it to sublist."
michael@0 5853 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
michael@0 5854 sublist.push(nodeList.shift());
michael@0 5855
michael@0 5856 // "Otherwise:"
michael@0 5857 } else {
michael@0 5858 // "Let nodes to wrap be a list of nodes, initially empty."
michael@0 5859 var nodesToWrap = [];
michael@0 5860
michael@0 5861 // "While nodes to wrap is empty, or node list is not empty
michael@0 5862 // and its first member is the nextSibling of nodes to
michael@0 5863 // wrap's last member and the first member of node list is
michael@0 5864 // an inline node and the last member of nodes to wrap is
michael@0 5865 // an inline node other than a br, remove the first member
michael@0 5866 // from node list and append it to nodes to wrap."
michael@0 5867 while (!nodesToWrap.length
michael@0 5868 || (nodeList.length
michael@0 5869 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
michael@0 5870 && isInlineNode(nodeList[0])
michael@0 5871 && isInlineNode(nodesToWrap[nodesToWrap.length - 1])
michael@0 5872 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
michael@0 5873 nodesToWrap.push(nodeList.shift());
michael@0 5874 }
michael@0 5875
michael@0 5876 // "Wrap nodes to wrap, with new parent instructions
michael@0 5877 // returning the result of calling createElement("li") on
michael@0 5878 // the context object. Append the result to sublist."
michael@0 5879 sublist.push(wrap(nodesToWrap,
michael@0 5880 undefined,
michael@0 5881 function() { return document.createElement("li") }));
michael@0 5882 }
michael@0 5883 }
michael@0 5884
michael@0 5885 // "If sublist's first member's parent is an HTML element with
michael@0 5886 // local name tag name, or if every member of sublist is an ol or
michael@0 5887 // ul, continue this loop from the beginning."
michael@0 5888 if (isHtmlElement(sublist[0].parentNode, tagName)
michael@0 5889 || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
michael@0 5890 continue;
michael@0 5891 }
michael@0 5892
michael@0 5893 // "If sublist's first member's parent is an HTML element with
michael@0 5894 // local name other tag name:"
michael@0 5895 if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
michael@0 5896 // "Record the values of sublist, and let values be the
michael@0 5897 // result."
michael@0 5898 var values = recordValues(sublist);
michael@0 5899
michael@0 5900 // "Split the parent of sublist."
michael@0 5901 splitParent(sublist);
michael@0 5902
michael@0 5903 // "Wrap sublist, with sibling criteria returning true for an
michael@0 5904 // HTML element with local name tag name and false otherwise,
michael@0 5905 // and new parent instructions returning the result of calling
michael@0 5906 // createElement(tag name) on the context object."
michael@0 5907 wrap(sublist,
michael@0 5908 function(node) { return isHtmlElement(node, tagName) },
michael@0 5909 function() { return document.createElement(tagName) });
michael@0 5910
michael@0 5911 // "Restore the values from values."
michael@0 5912 restoreValues(values);
michael@0 5913
michael@0 5914 // "Continue this loop from the beginning."
michael@0 5915 continue;
michael@0 5916 }
michael@0 5917
michael@0 5918 // "Wrap sublist, with sibling criteria returning true for an HTML
michael@0 5919 // element with local name tag name and false otherwise, and new
michael@0 5920 // parent instructions being the following:"
michael@0 5921 // . . .
michael@0 5922 // "Fix disallowed ancestors of the previous step's result."
michael@0 5923 fixDisallowedAncestors(wrap(sublist,
michael@0 5924 function(node) { return isHtmlElement(node, tagName) },
michael@0 5925 function() {
michael@0 5926 // "If sublist's first member's parent is not an editable
michael@0 5927 // simple indentation element, or sublist's first member's
michael@0 5928 // parent's previousSibling is not an editable HTML element
michael@0 5929 // with local name tag name, call createElement(tag name)
michael@0 5930 // on the context object and return the result."
michael@0 5931 if (!isEditable(sublist[0].parentNode)
michael@0 5932 || !isSimpleIndentationElement(sublist[0].parentNode)
michael@0 5933 || !isEditable(sublist[0].parentNode.previousSibling)
michael@0 5934 || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) {
michael@0 5935 return document.createElement(tagName);
michael@0 5936 }
michael@0 5937
michael@0 5938 // "Let list be sublist's first member's parent's
michael@0 5939 // previousSibling."
michael@0 5940 var list = sublist[0].parentNode.previousSibling;
michael@0 5941
michael@0 5942 // "Normalize sublists of list's lastChild."
michael@0 5943 normalizeSublists(list.lastChild);
michael@0 5944
michael@0 5945 // "If list's lastChild is not an editable HTML element
michael@0 5946 // with local name tag name, call createElement(tag name)
michael@0 5947 // on the context object, and append the result as the last
michael@0 5948 // child of list."
michael@0 5949 if (!isEditable(list.lastChild)
michael@0 5950 || !isHtmlElement(list.lastChild, tagName)) {
michael@0 5951 list.appendChild(document.createElement(tagName));
michael@0 5952 }
michael@0 5953
michael@0 5954 // "Return the last child of list."
michael@0 5955 return list.lastChild;
michael@0 5956 }
michael@0 5957 ));
michael@0 5958 }
michael@0 5959 }
michael@0 5960 }
michael@0 5961
michael@0 5962
michael@0 5963 //@}
michael@0 5964 ///// Justifying the selection /////
michael@0 5965 //@{
michael@0 5966
michael@0 5967 function justifySelection(alignment) {
michael@0 5968 // "Block-extend the active range, and let new range be the result."
michael@0 5969 var newRange = blockExtend(globalRange);
michael@0 5970
michael@0 5971 // "Let element list be a list of all editable Elements contained in new
michael@0 5972 // range that either has an attribute in the HTML namespace whose local
michael@0 5973 // name is "align", or has a style attribute that sets "text-align", or is
michael@0 5974 // a center."
michael@0 5975 var elementList = getAllContainedNodes(newRange, function(node) {
michael@0 5976 return node.nodeType == Node.ELEMENT_NODE
michael@0 5977 && isEditable(node)
michael@0 5978 // Ignoring namespaces here
michael@0 5979 && (
michael@0 5980 node.hasAttribute("align")
michael@0 5981 || node.style.textAlign != ""
michael@0 5982 || isHtmlElement(node, "center")
michael@0 5983 );
michael@0 5984 });
michael@0 5985
michael@0 5986 // "For each element in element list:"
michael@0 5987 for (var i = 0; i < elementList.length; i++) {
michael@0 5988 var element = elementList[i];
michael@0 5989
michael@0 5990 // "If element has an attribute in the HTML namespace whose local name
michael@0 5991 // is "align", remove that attribute."
michael@0 5992 element.removeAttribute("align");
michael@0 5993
michael@0 5994 // "Unset the CSS property "text-align" on element, if it's set by a
michael@0 5995 // style attribute."
michael@0 5996 element.style.textAlign = "";
michael@0 5997 if (element.getAttribute("style") == "") {
michael@0 5998 element.removeAttribute("style");
michael@0 5999 }
michael@0 6000
michael@0 6001 // "If element is a div or span or center with no attributes, remove
michael@0 6002 // it, preserving its descendants."
michael@0 6003 if (isHtmlElement(element, ["div", "span", "center"])
michael@0 6004 && !element.attributes.length) {
michael@0 6005 removePreservingDescendants(element);
michael@0 6006 }
michael@0 6007
michael@0 6008 // "If element is a center with one or more attributes, set the tag
michael@0 6009 // name of element to "div"."
michael@0 6010 if (isHtmlElement(element, "center")
michael@0 6011 && element.attributes.length) {
michael@0 6012 setTagName(element, "div");
michael@0 6013 }
michael@0 6014 }
michael@0 6015
michael@0 6016 // "Block-extend the active range, and let new range be the result."
michael@0 6017 newRange = blockExtend(globalRange);
michael@0 6018
michael@0 6019 // "Let node list be a list of nodes, initially empty."
michael@0 6020 var nodeList = [];
michael@0 6021
michael@0 6022 // "For each node node contained in new range, append node to node list if
michael@0 6023 // the last member of node list (if any) is not an ancestor of node; node
michael@0 6024 // is editable; node is an allowed child of "div"; and node's alignment
michael@0 6025 // value is not alignment."
michael@0 6026 nodeList = getContainedNodes(newRange, function(node) {
michael@0 6027 return isEditable(node)
michael@0 6028 && isAllowedChild(node, "div")
michael@0 6029 && getAlignmentValue(node) != alignment;
michael@0 6030 });
michael@0 6031
michael@0 6032 // "While node list is not empty:"
michael@0 6033 while (nodeList.length) {
michael@0 6034 // "Let sublist be a list of nodes, initially empty."
michael@0 6035 var sublist = [];
michael@0 6036
michael@0 6037 // "Remove the first member of node list and append it to sublist."
michael@0 6038 sublist.push(nodeList.shift());
michael@0 6039
michael@0 6040 // "While node list is not empty, and the first member of node list is
michael@0 6041 // the nextSibling of the last member of sublist, remove the first
michael@0 6042 // member of node list and append it to sublist."
michael@0 6043 while (nodeList.length
michael@0 6044 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
michael@0 6045 sublist.push(nodeList.shift());
michael@0 6046 }
michael@0 6047
michael@0 6048 // "Wrap sublist. Sibling criteria returns true for any div that has
michael@0 6049 // one or both of the following two attributes and no other attributes,
michael@0 6050 // and false otherwise:"
michael@0 6051 //
michael@0 6052 // * "An align attribute whose value is an ASCII case-insensitive
michael@0 6053 // match for alignment.
michael@0 6054 // * "A style attribute which sets exactly one CSS property
michael@0 6055 // (including unrecognized or invalid attributes), which is
michael@0 6056 // "text-align", which is set to alignment.
michael@0 6057 //
michael@0 6058 // "New parent instructions are to call createElement("div") on the
michael@0 6059 // context object, then set its CSS property "text-align" to alignment
michael@0 6060 // and return the result."
michael@0 6061 wrap(sublist,
michael@0 6062 function(node) {
michael@0 6063 return isHtmlElement(node, "div")
michael@0 6064 && [].every.call(node.attributes, function(attr) {
michael@0 6065 return (attr.name == "align" && attr.value.toLowerCase() == alignment)
michael@0 6066 || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment);
michael@0 6067 });
michael@0 6068 },
michael@0 6069 function() {
michael@0 6070 var newParent = document.createElement("div");
michael@0 6071 newParent.setAttribute("style", "text-align: " + alignment);
michael@0 6072 return newParent;
michael@0 6073 }
michael@0 6074 );
michael@0 6075 }
michael@0 6076 }
michael@0 6077
michael@0 6078
michael@0 6079 //@}
michael@0 6080 ///// Automatic linking /////
michael@0 6081 //@{
michael@0 6082 // "An autolinkable URL is a string of the following form:"
michael@0 6083 var autolinkableUrlRegexp =
michael@0 6084 // "Either a string matching the scheme pattern from RFC 3986 section 3.1
michael@0 6085 // followed by the literal string ://, or the literal string mailto:;
michael@0 6086 // followed by"
michael@0 6087 //
michael@0 6088 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
michael@0 6089 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
michael@0 6090 // "Zero or more characters other than space characters; followed by"
michael@0 6091 + "[^ \t\n\f\r]*"
michael@0 6092 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
michael@0 6093 + "[^!\"'(),\\-.:;<>[\\]`{}]";
michael@0 6094
michael@0 6095 // "A valid e-mail address is a string that matches the ABNF production 1*(
michael@0 6096 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
michael@0 6097 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
michael@0 6098 //
michael@0 6099 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
michael@0 6100 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
michael@0 6101 //
michael@0 6102 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
michael@0 6103 //<let-dig-hyp> ::= <let-dig> | "-"
michael@0 6104 //<let-dig> ::= <letter> | <digit>
michael@0 6105 var validEmailRegexp =
michael@0 6106 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
michael@0 6107
michael@0 6108 function autolink(node, endOffset) {
michael@0 6109 // "While (node, end offset)'s previous equivalent point is not null, set
michael@0 6110 // it to its previous equivalent point."
michael@0 6111 while (getPreviousEquivalentPoint(node, endOffset)) {
michael@0 6112 var prev = getPreviousEquivalentPoint(node, endOffset);
michael@0 6113 node = prev[0];
michael@0 6114 endOffset = prev[1];
michael@0 6115 }
michael@0 6116
michael@0 6117 // "If node is not a Text node, or has an a ancestor, do nothing and abort
michael@0 6118 // these steps."
michael@0 6119 if (node.nodeType != Node.TEXT_NODE
michael@0 6120 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) {
michael@0 6121 return;
michael@0 6122 }
michael@0 6123
michael@0 6124 // "Let search be the largest substring of node's data whose end is end
michael@0 6125 // offset and that contains no space characters."
michael@0 6126 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
michael@0 6127
michael@0 6128 // "If some substring of search is an autolinkable URL:"
michael@0 6129 if (new RegExp(autolinkableUrlRegexp).test(search)) {
michael@0 6130 // "While there is no substring of node's data ending at end offset
michael@0 6131 // that is an autolinkable URL, decrement end offset."
michael@0 6132 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) {
michael@0 6133 endOffset--;
michael@0 6134 }
michael@0 6135
michael@0 6136 // "Let start offset be the start index of the longest substring of
michael@0 6137 // node's data that is an autolinkable URL ending at end offset."
michael@0 6138 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index;
michael@0 6139
michael@0 6140 // "Let href be the substring of node's data starting at start offset
michael@0 6141 // and ending at end offset."
michael@0 6142 var href = node.substringData(startOffset, endOffset - startOffset);
michael@0 6143
michael@0 6144 // "Otherwise, if some substring of search is a valid e-mail address:"
michael@0 6145 } else if (new RegExp(validEmailRegexp).test(search)) {
michael@0 6146 // "While there is no substring of node's data ending at end offset
michael@0 6147 // that is a valid e-mail address, decrement end offset."
michael@0 6148 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) {
michael@0 6149 endOffset--;
michael@0 6150 }
michael@0 6151
michael@0 6152 // "Let start offset be the start index of the longest substring of
michael@0 6153 // node's data that is a valid e-mail address ending at end offset."
michael@0 6154 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index;
michael@0 6155
michael@0 6156 // "Let href be "mailto:" concatenated with the substring of node's
michael@0 6157 // data starting at start offset and ending at end offset."
michael@0 6158 var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset);
michael@0 6159
michael@0 6160 // "Otherwise, do nothing and abort these steps."
michael@0 6161 } else {
michael@0 6162 return;
michael@0 6163 }
michael@0 6164
michael@0 6165 // "Let original range be the active range."
michael@0 6166 var originalRange = getActiveRange();
michael@0 6167
michael@0 6168 // "Create a new range with start (node, start offset) and end (node, end
michael@0 6169 // offset), and set the context object's selection's range to it."
michael@0 6170 var newRange = document.createRange();
michael@0 6171 newRange.setStart(node, startOffset);
michael@0 6172 newRange.setEnd(node, endOffset);
michael@0 6173 getSelection().removeAllRanges();
michael@0 6174 getSelection().addRange(newRange);
michael@0 6175 globalRange = newRange;
michael@0 6176
michael@0 6177 // "Take the action for "createLink", with value equal to href."
michael@0 6178 commands.createlink.action(href);
michael@0 6179
michael@0 6180 // "Set the context object's selection's range to original range."
michael@0 6181 getSelection().removeAllRanges();
michael@0 6182 getSelection().addRange(originalRange);
michael@0 6183 globalRange = originalRange;
michael@0 6184 }
michael@0 6185 //@}
michael@0 6186 ///// The delete command /////
michael@0 6187 //@{
michael@0 6188 commands["delete"] = {
michael@0 6189 preservesOverrides: true,
michael@0 6190 action: function() {
michael@0 6191 // "If the active range is not collapsed, delete the selection and
michael@0 6192 // return true."
michael@0 6193 if (!getActiveRange().collapsed) {
michael@0 6194 deleteSelection();
michael@0 6195 return true;
michael@0 6196 }
michael@0 6197
michael@0 6198 // "Canonicalize whitespace at the active range's start."
michael@0 6199 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 6200
michael@0 6201 // "Let node and offset be the active range's start node and offset."
michael@0 6202 var node = getActiveRange().startContainer;
michael@0 6203 var offset = getActiveRange().startOffset;
michael@0 6204
michael@0 6205 // "Repeat the following steps:"
michael@0 6206 while (true) {
michael@0 6207 // "If offset is zero and node's previousSibling is an editable
michael@0 6208 // invisible node, remove node's previousSibling from its parent."
michael@0 6209 if (offset == 0
michael@0 6210 && isEditable(node.previousSibling)
michael@0 6211 && isInvisible(node.previousSibling)) {
michael@0 6212 node.parentNode.removeChild(node.previousSibling);
michael@0 6213
michael@0 6214 // "Otherwise, if node has a child with index offset − 1 and that
michael@0 6215 // child is an editable invisible node, remove that child from
michael@0 6216 // node, then subtract one from offset."
michael@0 6217 } else if (0 <= offset - 1
michael@0 6218 && offset - 1 < node.childNodes.length
michael@0 6219 && isEditable(node.childNodes[offset - 1])
michael@0 6220 && isInvisible(node.childNodes[offset - 1])) {
michael@0 6221 node.removeChild(node.childNodes[offset - 1]);
michael@0 6222 offset--;
michael@0 6223
michael@0 6224 // "Otherwise, if offset is zero and node is an inline node, or if
michael@0 6225 // node is an invisible node, set offset to the index of node, then
michael@0 6226 // set node to its parent."
michael@0 6227 } else if ((offset == 0
michael@0 6228 && isInlineNode(node))
michael@0 6229 || isInvisible(node)) {
michael@0 6230 offset = getNodeIndex(node);
michael@0 6231 node = node.parentNode;
michael@0 6232
michael@0 6233 // "Otherwise, if node has a child with index offset − 1 and that
michael@0 6234 // child is an editable a, remove that child from node, preserving
michael@0 6235 // its descendants. Then return true."
michael@0 6236 } else if (0 <= offset - 1
michael@0 6237 && offset - 1 < node.childNodes.length
michael@0 6238 && isEditable(node.childNodes[offset - 1])
michael@0 6239 && isHtmlElement(node.childNodes[offset - 1], "a")) {
michael@0 6240 removePreservingDescendants(node.childNodes[offset - 1]);
michael@0 6241 return true;
michael@0 6242
michael@0 6243 // "Otherwise, if node has a child with index offset − 1 and that
michael@0 6244 // child is not a block node or a br or an img, set node to that
michael@0 6245 // child, then set offset to the length of node."
michael@0 6246 } else if (0 <= offset - 1
michael@0 6247 && offset - 1 < node.childNodes.length
michael@0 6248 && !isBlockNode(node.childNodes[offset - 1])
michael@0 6249 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
michael@0 6250 node = node.childNodes[offset - 1];
michael@0 6251 offset = getNodeLength(node);
michael@0 6252
michael@0 6253 // "Otherwise, break from this loop."
michael@0 6254 } else {
michael@0 6255 break;
michael@0 6256 }
michael@0 6257 }
michael@0 6258
michael@0 6259 // "If node is a Text node and offset is not zero, or if node is a
michael@0 6260 // block node that has a child with index offset − 1 and that child is
michael@0 6261 // a br or hr or img:"
michael@0 6262 if ((node.nodeType == Node.TEXT_NODE
michael@0 6263 && offset != 0)
michael@0 6264 || (isBlockNode(node)
michael@0 6265 && 0 <= offset - 1
michael@0 6266 && offset - 1 < node.childNodes.length
michael@0 6267 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
michael@0 6268 // "Call collapse(node, offset) on the context object's Selection."
michael@0 6269 getSelection().collapse(node, offset);
michael@0 6270 getActiveRange().setEnd(node, offset);
michael@0 6271
michael@0 6272 // "Call extend(node, offset − 1) on the context object's
michael@0 6273 // Selection."
michael@0 6274 getSelection().extend(node, offset - 1);
michael@0 6275 getActiveRange().setStart(node, offset - 1);
michael@0 6276
michael@0 6277 // "Delete the selection."
michael@0 6278 deleteSelection();
michael@0 6279
michael@0 6280 // "Return true."
michael@0 6281 return true;
michael@0 6282 }
michael@0 6283
michael@0 6284 // "If node is an inline node, return true."
michael@0 6285 if (isInlineNode(node)) {
michael@0 6286 return true;
michael@0 6287 }
michael@0 6288
michael@0 6289 // "If node is an li or dt or dd and is the first child of its parent,
michael@0 6290 // and offset is zero:"
michael@0 6291 if (isHtmlElement(node, ["li", "dt", "dd"])
michael@0 6292 && node == node.parentNode.firstChild
michael@0 6293 && offset == 0) {
michael@0 6294 // "Let items be a list of all lis that are ancestors of node."
michael@0 6295 //
michael@0 6296 // Remember, must be in tree order.
michael@0 6297 var items = [];
michael@0 6298 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
michael@0 6299 if (isHtmlElement(ancestor, "li")) {
michael@0 6300 items.unshift(ancestor);
michael@0 6301 }
michael@0 6302 }
michael@0 6303
michael@0 6304 // "Normalize sublists of each item in items."
michael@0 6305 for (var i = 0; i < items.length; i++) {
michael@0 6306 normalizeSublists(items[i]);
michael@0 6307 }
michael@0 6308
michael@0 6309 // "Record the values of the one-node list consisting of node, and
michael@0 6310 // let values be the result."
michael@0 6311 var values = recordValues([node]);
michael@0 6312
michael@0 6313 // "Split the parent of the one-node list consisting of node."
michael@0 6314 splitParent([node]);
michael@0 6315
michael@0 6316 // "Restore the values from values."
michael@0 6317 restoreValues(values);
michael@0 6318
michael@0 6319 // "If node is a dd or dt, and it is not an allowed child of any of
michael@0 6320 // its ancestors in the same editing host, set the tag name of node
michael@0 6321 // to the default single-line container name and let node be the
michael@0 6322 // result."
michael@0 6323 if (isHtmlElement(node, ["dd", "dt"])
michael@0 6324 && getAncestors(node).every(function(ancestor) {
michael@0 6325 return !inSameEditingHost(node, ancestor)
michael@0 6326 || !isAllowedChild(node, ancestor)
michael@0 6327 })) {
michael@0 6328 node = setTagName(node, defaultSingleLineContainerName);
michael@0 6329 }
michael@0 6330
michael@0 6331 // "Fix disallowed ancestors of node."
michael@0 6332 fixDisallowedAncestors(node);
michael@0 6333
michael@0 6334 // "Return true."
michael@0 6335 return true;
michael@0 6336 }
michael@0 6337
michael@0 6338 // "Let start node equal node and let start offset equal offset."
michael@0 6339 var startNode = node;
michael@0 6340 var startOffset = offset;
michael@0 6341
michael@0 6342 // "Repeat the following steps:"
michael@0 6343 while (true) {
michael@0 6344 // "If start offset is zero, set start offset to the index of start
michael@0 6345 // node and then set start node to its parent."
michael@0 6346 if (startOffset == 0) {
michael@0 6347 startOffset = getNodeIndex(startNode);
michael@0 6348 startNode = startNode.parentNode;
michael@0 6349
michael@0 6350 // "Otherwise, if start node has an editable invisible child with
michael@0 6351 // index start offset minus one, remove it from start node and
michael@0 6352 // subtract one from start offset."
michael@0 6353 } else if (0 <= startOffset - 1
michael@0 6354 && startOffset - 1 < startNode.childNodes.length
michael@0 6355 && isEditable(startNode.childNodes[startOffset - 1])
michael@0 6356 && isInvisible(startNode.childNodes[startOffset - 1])) {
michael@0 6357 startNode.removeChild(startNode.childNodes[startOffset - 1]);
michael@0 6358 startOffset--;
michael@0 6359
michael@0 6360 // "Otherwise, break from this loop."
michael@0 6361 } else {
michael@0 6362 break;
michael@0 6363 }
michael@0 6364 }
michael@0 6365
michael@0 6366 // "If offset is zero, and node has an editable ancestor container in
michael@0 6367 // the same editing host that's an indentation element:"
michael@0 6368 if (offset == 0
michael@0 6369 && getAncestors(node).concat(node).filter(function(ancestor) {
michael@0 6370 return isEditable(ancestor)
michael@0 6371 && inSameEditingHost(ancestor, node)
michael@0 6372 && isIndentationElement(ancestor);
michael@0 6373 }).length) {
michael@0 6374 // "Block-extend the range whose start and end are both (node, 0),
michael@0 6375 // and let new range be the result."
michael@0 6376 var newRange = document.createRange();
michael@0 6377 newRange.setStart(node, 0);
michael@0 6378 newRange = blockExtend(newRange);
michael@0 6379
michael@0 6380 // "Let node list be a list of nodes, initially empty."
michael@0 6381 //
michael@0 6382 // "For each node current node contained in new range, append
michael@0 6383 // current node to node list if the last member of node list (if
michael@0 6384 // any) is not an ancestor of current node, and current node is
michael@0 6385 // editable but has no editable descendants."
michael@0 6386 var nodeList = getContainedNodes(newRange, function(currentNode) {
michael@0 6387 return isEditable(currentNode)
michael@0 6388 && !hasEditableDescendants(currentNode);
michael@0 6389 });
michael@0 6390
michael@0 6391 // "Outdent each node in node list."
michael@0 6392 for (var i = 0; i < nodeList.length; i++) {
michael@0 6393 outdentNode(nodeList[i]);
michael@0 6394 }
michael@0 6395
michael@0 6396 // "Return true."
michael@0 6397 return true;
michael@0 6398 }
michael@0 6399
michael@0 6400 // "If the child of start node with index start offset is a table,
michael@0 6401 // return true."
michael@0 6402 if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
michael@0 6403 return true;
michael@0 6404 }
michael@0 6405
michael@0 6406 // "If start node has a child with index start offset − 1, and that
michael@0 6407 // child is a table:"
michael@0 6408 if (0 <= startOffset - 1
michael@0 6409 && startOffset - 1 < startNode.childNodes.length
michael@0 6410 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
michael@0 6411 // "Call collapse(start node, start offset − 1) on the context
michael@0 6412 // object's Selection."
michael@0 6413 getSelection().collapse(startNode, startOffset - 1);
michael@0 6414 getActiveRange().setStart(startNode, startOffset - 1);
michael@0 6415
michael@0 6416 // "Call extend(start node, start offset) on the context object's
michael@0 6417 // Selection."
michael@0 6418 getSelection().extend(startNode, startOffset);
michael@0 6419 getActiveRange().setEnd(startNode, startOffset);
michael@0 6420
michael@0 6421 // "Return true."
michael@0 6422 return true;
michael@0 6423 }
michael@0 6424
michael@0 6425 // "If offset is zero; and either the child of start node with index
michael@0 6426 // start offset minus one is an hr, or the child is a br whose
michael@0 6427 // previousSibling is either a br or not an inline node:"
michael@0 6428 if (offset == 0
michael@0 6429 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
michael@0 6430 || (
michael@0 6431 isHtmlElement(startNode.childNodes[startOffset - 1], "br")
michael@0 6432 && (
michael@0 6433 isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
michael@0 6434 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
michael@0 6435 )
michael@0 6436 )
michael@0 6437 )) {
michael@0 6438 // "Call collapse(start node, start offset − 1) on the context
michael@0 6439 // object's Selection."
michael@0 6440 getSelection().collapse(startNode, startOffset - 1);
michael@0 6441 getActiveRange().setStart(startNode, startOffset - 1);
michael@0 6442
michael@0 6443 // "Call extend(start node, start offset) on the context object's
michael@0 6444 // Selection."
michael@0 6445 getSelection().extend(startNode, startOffset);
michael@0 6446 getActiveRange().setEnd(startNode, startOffset);
michael@0 6447
michael@0 6448 // "Delete the selection."
michael@0 6449 deleteSelection();
michael@0 6450
michael@0 6451 // "Call collapse(node, offset) on the Selection."
michael@0 6452 getSelection().collapse(node, offset);
michael@0 6453 getActiveRange().setStart(node, offset);
michael@0 6454 getActiveRange().collapse(true);
michael@0 6455
michael@0 6456 // "Return true."
michael@0 6457 return true;
michael@0 6458 }
michael@0 6459
michael@0 6460 // "If the child of start node with index start offset is an li or dt
michael@0 6461 // or dd, and that child's firstChild is an inline node, and start
michael@0 6462 // offset is not zero:"
michael@0 6463 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
michael@0 6464 && isInlineNode(startNode.childNodes[startOffset].firstChild)
michael@0 6465 && startOffset != 0) {
michael@0 6466 // "Let previous item be the child of start node with index start
michael@0 6467 // offset minus one."
michael@0 6468 var previousItem = startNode.childNodes[startOffset - 1];
michael@0 6469
michael@0 6470 // "If previous item's lastChild is an inline node other than a br,
michael@0 6471 // call createElement("br") on the context object and append the
michael@0 6472 // result as the last child of previous item."
michael@0 6473 if (isInlineNode(previousItem.lastChild)
michael@0 6474 && !isHtmlElement(previousItem.lastChild, "br")) {
michael@0 6475 previousItem.appendChild(document.createElement("br"));
michael@0 6476 }
michael@0 6477
michael@0 6478 // "If previous item's lastChild is an inline node, call
michael@0 6479 // createElement("br") on the context object and append the result
michael@0 6480 // as the last child of previous item."
michael@0 6481 if (isInlineNode(previousItem.lastChild)) {
michael@0 6482 previousItem.appendChild(document.createElement("br"));
michael@0 6483 }
michael@0 6484 }
michael@0 6485
michael@0 6486 // "If start node's child with index start offset is an li or dt or dd,
michael@0 6487 // and that child's previousSibling is also an li or dt or dd:"
michael@0 6488 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
michael@0 6489 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) {
michael@0 6490 // "Call cloneRange() on the active range, and let original range
michael@0 6491 // be the result."
michael@0 6492 //
michael@0 6493 // We need to add it to extraRanges so it will actually get updated
michael@0 6494 // when moving preserving ranges.
michael@0 6495 var originalRange = getActiveRange().cloneRange();
michael@0 6496 extraRanges.push(originalRange);
michael@0 6497
michael@0 6498 // "Set start node to its child with index start offset − 1."
michael@0 6499 startNode = startNode.childNodes[startOffset - 1];
michael@0 6500
michael@0 6501 // "Set start offset to start node's length."
michael@0 6502 startOffset = getNodeLength(startNode);
michael@0 6503
michael@0 6504 // "Set node to start node's nextSibling."
michael@0 6505 node = startNode.nextSibling;
michael@0 6506
michael@0 6507 // "Call collapse(start node, start offset) on the context object's
michael@0 6508 // Selection."
michael@0 6509 getSelection().collapse(startNode, startOffset);
michael@0 6510 getActiveRange().setStart(startNode, startOffset);
michael@0 6511
michael@0 6512 // "Call extend(node, 0) on the context object's Selection."
michael@0 6513 getSelection().extend(node, 0);
michael@0 6514 getActiveRange().setEnd(node, 0);
michael@0 6515
michael@0 6516 // "Delete the selection."
michael@0 6517 deleteSelection();
michael@0 6518
michael@0 6519 // "Call removeAllRanges() on the context object's Selection."
michael@0 6520 getSelection().removeAllRanges();
michael@0 6521
michael@0 6522 // "Call addRange(original range) on the context object's
michael@0 6523 // Selection."
michael@0 6524 getSelection().addRange(originalRange);
michael@0 6525 getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset);
michael@0 6526 getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset);
michael@0 6527
michael@0 6528 // "Return true."
michael@0 6529 extraRanges.pop();
michael@0 6530 return true;
michael@0 6531 }
michael@0 6532
michael@0 6533 // "While start node has a child with index start offset minus one:"
michael@0 6534 while (0 <= startOffset - 1
michael@0 6535 && startOffset - 1 < startNode.childNodes.length) {
michael@0 6536 // "If start node's child with index start offset minus one is
michael@0 6537 // editable and invisible, remove it from start node, then subtract
michael@0 6538 // one from start offset."
michael@0 6539 if (isEditable(startNode.childNodes[startOffset - 1])
michael@0 6540 && isInvisible(startNode.childNodes[startOffset - 1])) {
michael@0 6541 startNode.removeChild(startNode.childNodes[startOffset - 1]);
michael@0 6542 startOffset--;
michael@0 6543
michael@0 6544 // "Otherwise, set start node to its child with index start offset
michael@0 6545 // minus one, then set start offset to the length of start node."
michael@0 6546 } else {
michael@0 6547 startNode = startNode.childNodes[startOffset - 1];
michael@0 6548 startOffset = getNodeLength(startNode);
michael@0 6549 }
michael@0 6550 }
michael@0 6551
michael@0 6552 // "Call collapse(start node, start offset) on the context object's
michael@0 6553 // Selection."
michael@0 6554 getSelection().collapse(startNode, startOffset);
michael@0 6555 getActiveRange().setStart(startNode, startOffset);
michael@0 6556
michael@0 6557 // "Call extend(node, offset) on the context object's Selection."
michael@0 6558 getSelection().extend(node, offset);
michael@0 6559 getActiveRange().setEnd(node, offset);
michael@0 6560
michael@0 6561 // "Delete the selection, with direction "backward"."
michael@0 6562 deleteSelection({direction: "backward"});
michael@0 6563
michael@0 6564 // "Return true."
michael@0 6565 return true;
michael@0 6566 }
michael@0 6567 };
michael@0 6568
michael@0 6569 //@}
michael@0 6570 ///// The formatBlock command /////
michael@0 6571 //@{
michael@0 6572 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
michael@0 6573 // "h4", "h5", "h6", "p", or "pre"."
michael@0 6574 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
michael@0 6575 "h4", "h5", "h6", "p", "pre"];
michael@0 6576
michael@0 6577 commands.formatblock = {
michael@0 6578 preservesOverrides: true,
michael@0 6579 action: function(value) {
michael@0 6580 // "If value begins with a "<" character and ends with a ">" character,
michael@0 6581 // remove the first and last characters from it."
michael@0 6582 if (/^<.*>$/.test(value)) {
michael@0 6583 value = value.slice(1, -1);
michael@0 6584 }
michael@0 6585
michael@0 6586 // "Let value be converted to ASCII lowercase."
michael@0 6587 value = value.toLowerCase();
michael@0 6588
michael@0 6589 // "If value is not a formattable block name, return false."
michael@0 6590 if (formattableBlockNames.indexOf(value) == -1) {
michael@0 6591 return false;
michael@0 6592 }
michael@0 6593
michael@0 6594 // "Block-extend the active range, and let new range be the result."
michael@0 6595 var newRange = blockExtend(getActiveRange());
michael@0 6596
michael@0 6597 // "Let node list be an empty list of nodes."
michael@0 6598 //
michael@0 6599 // "For each node node contained in new range, append node to node list
michael@0 6600 // if it is editable, the last member of original node list (if any) is
michael@0 6601 // not an ancestor of node, node is either a non-list single-line
michael@0 6602 // container or an allowed child of "p" or a dd or dt, and node is not
michael@0 6603 // the ancestor of a prohibited paragraph child."
michael@0 6604 var nodeList = getContainedNodes(newRange, function(node) {
michael@0 6605 return isEditable(node)
michael@0 6606 && (isNonListSingleLineContainer(node)
michael@0 6607 || isAllowedChild(node, "p")
michael@0 6608 || isHtmlElement(node, ["dd", "dt"]))
michael@0 6609 && !getDescendants(node).some(isProhibitedParagraphChild);
michael@0 6610 });
michael@0 6611
michael@0 6612 // "Record the values of node list, and let values be the result."
michael@0 6613 var values = recordValues(nodeList);
michael@0 6614
michael@0 6615 // "For each node in node list, while node is the descendant of an
michael@0 6616 // editable HTML element in the same editing host, whose local name is
michael@0 6617 // a formattable block name, and which is not the ancestor of a
michael@0 6618 // prohibited paragraph child, split the parent of the one-node list
michael@0 6619 // consisting of node."
michael@0 6620 for (var i = 0; i < nodeList.length; i++) {
michael@0 6621 var node = nodeList[i];
michael@0 6622 while (getAncestors(node).some(function(ancestor) {
michael@0 6623 return isEditable(ancestor)
michael@0 6624 && inSameEditingHost(ancestor, node)
michael@0 6625 && isHtmlElement(ancestor, formattableBlockNames)
michael@0 6626 && !getDescendants(ancestor).some(isProhibitedParagraphChild);
michael@0 6627 })) {
michael@0 6628 splitParent([node]);
michael@0 6629 }
michael@0 6630 }
michael@0 6631
michael@0 6632 // "Restore the values from values."
michael@0 6633 restoreValues(values);
michael@0 6634
michael@0 6635 // "While node list is not empty:"
michael@0 6636 while (nodeList.length) {
michael@0 6637 var sublist;
michael@0 6638
michael@0 6639 // "If the first member of node list is a single-line
michael@0 6640 // container:"
michael@0 6641 if (isSingleLineContainer(nodeList[0])) {
michael@0 6642 // "Let sublist be the children of the first member of node
michael@0 6643 // list."
michael@0 6644 sublist = [].slice.call(nodeList[0].childNodes);
michael@0 6645
michael@0 6646 // "Record the values of sublist, and let values be the
michael@0 6647 // result."
michael@0 6648 var values = recordValues(sublist);
michael@0 6649
michael@0 6650 // "Remove the first member of node list from its parent,
michael@0 6651 // preserving its descendants."
michael@0 6652 removePreservingDescendants(nodeList[0]);
michael@0 6653
michael@0 6654 // "Restore the values from values."
michael@0 6655 restoreValues(values);
michael@0 6656
michael@0 6657 // "Remove the first member from node list."
michael@0 6658 nodeList.shift();
michael@0 6659
michael@0 6660 // "Otherwise:"
michael@0 6661 } else {
michael@0 6662 // "Let sublist be an empty list of nodes."
michael@0 6663 sublist = [];
michael@0 6664
michael@0 6665 // "Remove the first member of node list and append it to
michael@0 6666 // sublist."
michael@0 6667 sublist.push(nodeList.shift());
michael@0 6668
michael@0 6669 // "While node list is not empty, and the first member of
michael@0 6670 // node list is the nextSibling of the last member of
michael@0 6671 // sublist, and the first member of node list is not a
michael@0 6672 // single-line container, and the last member of sublist is
michael@0 6673 // not a br, remove the first member of node list and
michael@0 6674 // append it to sublist."
michael@0 6675 while (nodeList.length
michael@0 6676 && nodeList[0] == sublist[sublist.length - 1].nextSibling
michael@0 6677 && !isSingleLineContainer(nodeList[0])
michael@0 6678 && !isHtmlElement(sublist[sublist.length - 1], "BR")) {
michael@0 6679 sublist.push(nodeList.shift());
michael@0 6680 }
michael@0 6681 }
michael@0 6682
michael@0 6683 // "Wrap sublist. If value is "div" or "p", sibling criteria
michael@0 6684 // returns false; otherwise it returns true for an HTML element
michael@0 6685 // with local name value and no attributes, and false otherwise.
michael@0 6686 // New parent instructions return the result of running
michael@0 6687 // createElement(value) on the context object. Then fix disallowed
michael@0 6688 // ancestors of the result."
michael@0 6689 fixDisallowedAncestors(wrap(sublist,
michael@0 6690 ["div", "p"].indexOf(value) == - 1
michael@0 6691 ? function(node) { return isHtmlElement(node, value) && !node.attributes.length }
michael@0 6692 : function() { return false },
michael@0 6693 function() { return document.createElement(value) }));
michael@0 6694 }
michael@0 6695
michael@0 6696 // "Return true."
michael@0 6697 return true;
michael@0 6698 }, indeterm: function() {
michael@0 6699 // "If the active range is null, return false."
michael@0 6700 if (!getActiveRange()) {
michael@0 6701 return false;
michael@0 6702 }
michael@0 6703
michael@0 6704 // "Block-extend the active range, and let new range be the result."
michael@0 6705 var newRange = blockExtend(getActiveRange());
michael@0 6706
michael@0 6707 // "Let node list be all visible editable nodes that are contained in
michael@0 6708 // new range and have no children."
michael@0 6709 var nodeList = getAllContainedNodes(newRange, function(node) {
michael@0 6710 return isVisible(node)
michael@0 6711 && isEditable(node)
michael@0 6712 && !node.hasChildNodes();
michael@0 6713 });
michael@0 6714
michael@0 6715 // "If node list is empty, return false."
michael@0 6716 if (!nodeList.length) {
michael@0 6717 return false;
michael@0 6718 }
michael@0 6719
michael@0 6720 // "Let type be null."
michael@0 6721 var type = null;
michael@0 6722
michael@0 6723 // "For each node in node list:"
michael@0 6724 for (var i = 0; i < nodeList.length; i++) {
michael@0 6725 var node = nodeList[i];
michael@0 6726
michael@0 6727 // "While node's parent is editable and in the same editing host as
michael@0 6728 // node, and node is not an HTML element whose local name is a
michael@0 6729 // formattable block name, set node to its parent."
michael@0 6730 while (isEditable(node.parentNode)
michael@0 6731 && inSameEditingHost(node, node.parentNode)
michael@0 6732 && !isHtmlElement(node, formattableBlockNames)) {
michael@0 6733 node = node.parentNode;
michael@0 6734 }
michael@0 6735
michael@0 6736 // "Let current type be the empty string."
michael@0 6737 var currentType = "";
michael@0 6738
michael@0 6739 // "If node is an editable HTML element whose local name is a
michael@0 6740 // formattable block name, and node is not the ancestor of a
michael@0 6741 // prohibited paragraph child, set current type to node's local
michael@0 6742 // name."
michael@0 6743 if (isEditable(node)
michael@0 6744 && isHtmlElement(node, formattableBlockNames)
michael@0 6745 && !getDescendants(node).some(isProhibitedParagraphChild)) {
michael@0 6746 currentType = node.tagName;
michael@0 6747 }
michael@0 6748
michael@0 6749 // "If type is null, set type to current type."
michael@0 6750 if (type === null) {
michael@0 6751 type = currentType;
michael@0 6752
michael@0 6753 // "Otherwise, if type does not equal current type, return true."
michael@0 6754 } else if (type != currentType) {
michael@0 6755 return true;
michael@0 6756 }
michael@0 6757 }
michael@0 6758
michael@0 6759 // "Return false."
michael@0 6760 return false;
michael@0 6761 }, value: function() {
michael@0 6762 // "If the active range is null, return the empty string."
michael@0 6763 if (!getActiveRange()) {
michael@0 6764 return "";
michael@0 6765 }
michael@0 6766
michael@0 6767 // "Block-extend the active range, and let new range be the result."
michael@0 6768 var newRange = blockExtend(getActiveRange());
michael@0 6769
michael@0 6770 // "Let node be the first visible editable node that is contained in
michael@0 6771 // new range and has no children. If there is no such node, return the
michael@0 6772 // empty string."
michael@0 6773 var nodes = getAllContainedNodes(newRange, function(node) {
michael@0 6774 return isVisible(node)
michael@0 6775 && isEditable(node)
michael@0 6776 && !node.hasChildNodes();
michael@0 6777 });
michael@0 6778 if (!nodes.length) {
michael@0 6779 return "";
michael@0 6780 }
michael@0 6781 var node = nodes[0];
michael@0 6782
michael@0 6783 // "While node's parent is editable and in the same editing host as
michael@0 6784 // node, and node is not an HTML element whose local name is a
michael@0 6785 // formattable block name, set node to its parent."
michael@0 6786 while (isEditable(node.parentNode)
michael@0 6787 && inSameEditingHost(node, node.parentNode)
michael@0 6788 && !isHtmlElement(node, formattableBlockNames)) {
michael@0 6789 node = node.parentNode;
michael@0 6790 }
michael@0 6791
michael@0 6792 // "If node is an editable HTML element whose local name is a
michael@0 6793 // formattable block name, and node is not the ancestor of a prohibited
michael@0 6794 // paragraph child, return node's local name, converted to ASCII
michael@0 6795 // lowercase."
michael@0 6796 if (isEditable(node)
michael@0 6797 && isHtmlElement(node, formattableBlockNames)
michael@0 6798 && !getDescendants(node).some(isProhibitedParagraphChild)) {
michael@0 6799 return node.tagName.toLowerCase();
michael@0 6800 }
michael@0 6801
michael@0 6802 // "Return the empty string."
michael@0 6803 return "";
michael@0 6804 }
michael@0 6805 };
michael@0 6806
michael@0 6807 //@}
michael@0 6808 ///// The forwardDelete command /////
michael@0 6809 //@{
michael@0 6810 commands.forwarddelete = {
michael@0 6811 preservesOverrides: true,
michael@0 6812 action: function() {
michael@0 6813 // "If the active range is not collapsed, delete the selection and
michael@0 6814 // return true."
michael@0 6815 if (!getActiveRange().collapsed) {
michael@0 6816 deleteSelection();
michael@0 6817 return true;
michael@0 6818 }
michael@0 6819
michael@0 6820 // "Canonicalize whitespace at the active range's start."
michael@0 6821 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 6822
michael@0 6823 // "Let node and offset be the active range's start node and offset."
michael@0 6824 var node = getActiveRange().startContainer;
michael@0 6825 var offset = getActiveRange().startOffset;
michael@0 6826
michael@0 6827 // "Repeat the following steps:"
michael@0 6828 while (true) {
michael@0 6829 // "If offset is the length of node and node's nextSibling is an
michael@0 6830 // editable invisible node, remove node's nextSibling from its
michael@0 6831 // parent."
michael@0 6832 if (offset == getNodeLength(node)
michael@0 6833 && isEditable(node.nextSibling)
michael@0 6834 && isInvisible(node.nextSibling)) {
michael@0 6835 node.parentNode.removeChild(node.nextSibling);
michael@0 6836
michael@0 6837 // "Otherwise, if node has a child with index offset and that child
michael@0 6838 // is an editable invisible node, remove that child from node."
michael@0 6839 } else if (offset < node.childNodes.length
michael@0 6840 && isEditable(node.childNodes[offset])
michael@0 6841 && isInvisible(node.childNodes[offset])) {
michael@0 6842 node.removeChild(node.childNodes[offset]);
michael@0 6843
michael@0 6844 // "Otherwise, if offset is the length of node and node is an
michael@0 6845 // inline node, or if node is invisible, set offset to one plus the
michael@0 6846 // index of node, then set node to its parent."
michael@0 6847 } else if ((offset == getNodeLength(node)
michael@0 6848 && isInlineNode(node))
michael@0 6849 || isInvisible(node)) {
michael@0 6850 offset = 1 + getNodeIndex(node);
michael@0 6851 node = node.parentNode;
michael@0 6852
michael@0 6853 // "Otherwise, if node has a child with index offset and that child
michael@0 6854 // is neither a block node nor a br nor an img nor a collapsed
michael@0 6855 // block prop, set node to that child, then set offset to zero."
michael@0 6856 } else if (offset < node.childNodes.length
michael@0 6857 && !isBlockNode(node.childNodes[offset])
michael@0 6858 && !isHtmlElement(node.childNodes[offset], ["br", "img"])
michael@0 6859 && !isCollapsedBlockProp(node.childNodes[offset])) {
michael@0 6860 node = node.childNodes[offset];
michael@0 6861 offset = 0;
michael@0 6862
michael@0 6863 // "Otherwise, break from this loop."
michael@0 6864 } else {
michael@0 6865 break;
michael@0 6866 }
michael@0 6867 }
michael@0 6868
michael@0 6869 // "If node is a Text node and offset is not node's length:"
michael@0 6870 if (node.nodeType == Node.TEXT_NODE
michael@0 6871 && offset != getNodeLength(node)) {
michael@0 6872 // "Let end offset be offset plus one."
michael@0 6873 var endOffset = offset + 1;
michael@0 6874
michael@0 6875 // "While end offset is not node's length and the end offsetth
michael@0 6876 // element of node's data has general category M when interpreted
michael@0 6877 // as a Unicode code point, add one to end offset."
michael@0 6878 //
michael@0 6879 // TODO: Not even going to try handling anything beyond the most
michael@0 6880 // basic combining marks, since I couldn't find a good list. I
michael@0 6881 // special-case a few Hebrew diacritics too to test basic coverage
michael@0 6882 // of non-Latin stuff.
michael@0 6883 while (endOffset != node.length
michael@0 6884 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
michael@0 6885 endOffset++;
michael@0 6886 }
michael@0 6887
michael@0 6888 // "Call collapse(node, offset) on the context object's Selection."
michael@0 6889 getSelection().collapse(node, offset);
michael@0 6890 getActiveRange().setStart(node, offset);
michael@0 6891
michael@0 6892 // "Call extend(node, end offset) on the context object's
michael@0 6893 // Selection."
michael@0 6894 getSelection().extend(node, endOffset);
michael@0 6895 getActiveRange().setEnd(node, endOffset);
michael@0 6896
michael@0 6897 // "Delete the selection."
michael@0 6898 deleteSelection();
michael@0 6899
michael@0 6900 // "Return true."
michael@0 6901 return true;
michael@0 6902 }
michael@0 6903
michael@0 6904 // "If node is an inline node, return true."
michael@0 6905 if (isInlineNode(node)) {
michael@0 6906 return true;
michael@0 6907 }
michael@0 6908
michael@0 6909 // "If node has a child with index offset and that child is a br or hr
michael@0 6910 // or img, but is not a collapsed block prop:"
michael@0 6911 if (offset < node.childNodes.length
michael@0 6912 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
michael@0 6913 && !isCollapsedBlockProp(node.childNodes[offset])) {
michael@0 6914 // "Call collapse(node, offset) on the context object's Selection."
michael@0 6915 getSelection().collapse(node, offset);
michael@0 6916 getActiveRange().setStart(node, offset);
michael@0 6917
michael@0 6918 // "Call extend(node, offset + 1) on the context object's
michael@0 6919 // Selection."
michael@0 6920 getSelection().extend(node, offset + 1);
michael@0 6921 getActiveRange().setEnd(node, offset + 1);
michael@0 6922
michael@0 6923 // "Delete the selection."
michael@0 6924 deleteSelection();
michael@0 6925
michael@0 6926 // "Return true."
michael@0 6927 return true;
michael@0 6928 }
michael@0 6929
michael@0 6930 // "Let end node equal node and let end offset equal offset."
michael@0 6931 var endNode = node;
michael@0 6932 var endOffset = offset;
michael@0 6933
michael@0 6934 // "If end node has a child with index end offset, and that child is a
michael@0 6935 // collapsed block prop, add one to end offset."
michael@0 6936 if (endOffset < endNode.childNodes.length
michael@0 6937 && isCollapsedBlockProp(endNode.childNodes[endOffset])) {
michael@0 6938 endOffset++;
michael@0 6939 }
michael@0 6940
michael@0 6941 // "Repeat the following steps:"
michael@0 6942 while (true) {
michael@0 6943 // "If end offset is the length of end node, set end offset to one
michael@0 6944 // plus the index of end node and then set end node to its parent."
michael@0 6945 if (endOffset == getNodeLength(endNode)) {
michael@0 6946 endOffset = 1 + getNodeIndex(endNode);
michael@0 6947 endNode = endNode.parentNode;
michael@0 6948
michael@0 6949 // "Otherwise, if end node has a an editable invisible child with
michael@0 6950 // index end offset, remove it from end node."
michael@0 6951 } else if (endOffset < endNode.childNodes.length
michael@0 6952 && isEditable(endNode.childNodes[endOffset])
michael@0 6953 && isInvisible(endNode.childNodes[endOffset])) {
michael@0 6954 endNode.removeChild(endNode.childNodes[endOffset]);
michael@0 6955
michael@0 6956 // "Otherwise, break from this loop."
michael@0 6957 } else {
michael@0 6958 break;
michael@0 6959 }
michael@0 6960 }
michael@0 6961
michael@0 6962 // "If the child of end node with index end offset minus one is a
michael@0 6963 // table, return true."
michael@0 6964 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
michael@0 6965 return true;
michael@0 6966 }
michael@0 6967
michael@0 6968 // "If the child of end node with index end offset is a table:"
michael@0 6969 if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
michael@0 6970 // "Call collapse(end node, end offset) on the context object's
michael@0 6971 // Selection."
michael@0 6972 getSelection().collapse(endNode, endOffset);
michael@0 6973 getActiveRange().setStart(endNode, endOffset);
michael@0 6974
michael@0 6975 // "Call extend(end node, end offset + 1) on the context object's
michael@0 6976 // Selection."
michael@0 6977 getSelection().extend(endNode, endOffset + 1);
michael@0 6978 getActiveRange().setEnd(endNode, endOffset + 1);
michael@0 6979
michael@0 6980 // "Return true."
michael@0 6981 return true;
michael@0 6982 }
michael@0 6983
michael@0 6984 // "If offset is the length of node, and the child of end node with
michael@0 6985 // index end offset is an hr or br:"
michael@0 6986 if (offset == getNodeLength(node)
michael@0 6987 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
michael@0 6988 // "Call collapse(end node, end offset) on the context object's
michael@0 6989 // Selection."
michael@0 6990 getSelection().collapse(endNode, endOffset);
michael@0 6991 getActiveRange().setStart(endNode, endOffset);
michael@0 6992
michael@0 6993 // "Call extend(end node, end offset + 1) on the context object's
michael@0 6994 // Selection."
michael@0 6995 getSelection().extend(endNode, endOffset + 1);
michael@0 6996 getActiveRange().setEnd(endNode, endOffset + 1);
michael@0 6997
michael@0 6998 // "Delete the selection."
michael@0 6999 deleteSelection();
michael@0 7000
michael@0 7001 // "Call collapse(node, offset) on the Selection."
michael@0 7002 getSelection().collapse(node, offset);
michael@0 7003 getActiveRange().setStart(node, offset);
michael@0 7004 getActiveRange().collapse(true);
michael@0 7005
michael@0 7006 // "Return true."
michael@0 7007 return true;
michael@0 7008 }
michael@0 7009
michael@0 7010 // "While end node has a child with index end offset:"
michael@0 7011 while (endOffset < endNode.childNodes.length) {
michael@0 7012 // "If end node's child with index end offset is editable and
michael@0 7013 // invisible, remove it from end node."
michael@0 7014 if (isEditable(endNode.childNodes[endOffset])
michael@0 7015 && isInvisible(endNode.childNodes[endOffset])) {
michael@0 7016 endNode.removeChild(endNode.childNodes[endOffset]);
michael@0 7017
michael@0 7018 // "Otherwise, set end node to its child with index end offset and
michael@0 7019 // set end offset to zero."
michael@0 7020 } else {
michael@0 7021 endNode = endNode.childNodes[endOffset];
michael@0 7022 endOffset = 0;
michael@0 7023 }
michael@0 7024 }
michael@0 7025
michael@0 7026 // "Call collapse(node, offset) on the context object's Selection."
michael@0 7027 getSelection().collapse(node, offset);
michael@0 7028 getActiveRange().setStart(node, offset);
michael@0 7029
michael@0 7030 // "Call extend(end node, end offset) on the context object's
michael@0 7031 // Selection."
michael@0 7032 getSelection().extend(endNode, endOffset);
michael@0 7033 getActiveRange().setEnd(endNode, endOffset);
michael@0 7034
michael@0 7035 // "Delete the selection."
michael@0 7036 deleteSelection();
michael@0 7037
michael@0 7038 // "Return true."
michael@0 7039 return true;
michael@0 7040 }
michael@0 7041 };
michael@0 7042
michael@0 7043 //@}
michael@0 7044 ///// The indent command /////
michael@0 7045 //@{
michael@0 7046 commands.indent = {
michael@0 7047 preservesOverrides: true,
michael@0 7048 action: function() {
michael@0 7049 // "Let items be a list of all lis that are ancestor containers of the
michael@0 7050 // active range's start and/or end node."
michael@0 7051 //
michael@0 7052 // Has to be in tree order, remember!
michael@0 7053 var items = [];
michael@0 7054 for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
michael@0 7055 if (isHtmlElement(node, "LI")) {
michael@0 7056 items.unshift(node);
michael@0 7057 }
michael@0 7058 }
michael@0 7059 for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
michael@0 7060 if (isHtmlElement(node, "LI")) {
michael@0 7061 items.unshift(node);
michael@0 7062 }
michael@0 7063 }
michael@0 7064 for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
michael@0 7065 if (isHtmlElement(node, "LI")) {
michael@0 7066 items.unshift(node);
michael@0 7067 }
michael@0 7068 }
michael@0 7069
michael@0 7070 // "For each item in items, normalize sublists of item."
michael@0 7071 for (var i = 0; i < items.length; i++) {
michael@0 7072 normalizeSublists(items[i]);
michael@0 7073 }
michael@0 7074
michael@0 7075 // "Block-extend the active range, and let new range be the result."
michael@0 7076 var newRange = blockExtend(getActiveRange());
michael@0 7077
michael@0 7078 // "Let node list be a list of nodes, initially empty."
michael@0 7079 var nodeList = [];
michael@0 7080
michael@0 7081 // "For each node node contained in new range, if node is editable and
michael@0 7082 // is an allowed child of "div" or "ol" and if the last member of node
michael@0 7083 // list (if any) is not an ancestor of node, append node to node list."
michael@0 7084 nodeList = getContainedNodes(newRange, function(node) {
michael@0 7085 return isEditable(node)
michael@0 7086 && (isAllowedChild(node, "div")
michael@0 7087 || isAllowedChild(node, "ol"));
michael@0 7088 });
michael@0 7089
michael@0 7090 // "If the first visible member of node list is an li whose parent is
michael@0 7091 // an ol or ul:"
michael@0 7092 if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
michael@0 7093 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) {
michael@0 7094 // "Let sibling be node list's first visible member's
michael@0 7095 // previousSibling."
michael@0 7096 var sibling = nodeList.filter(isVisible)[0].previousSibling;
michael@0 7097
michael@0 7098 // "While sibling is invisible, set sibling to its
michael@0 7099 // previousSibling."
michael@0 7100 while (isInvisible(sibling)) {
michael@0 7101 sibling = sibling.previousSibling;
michael@0 7102 }
michael@0 7103
michael@0 7104 // "If sibling is an li, normalize sublists of sibling."
michael@0 7105 if (isHtmlElement(sibling, "li")) {
michael@0 7106 normalizeSublists(sibling);
michael@0 7107 }
michael@0 7108 }
michael@0 7109
michael@0 7110 // "While node list is not empty:"
michael@0 7111 while (nodeList.length) {
michael@0 7112 // "Let sublist be a list of nodes, initially empty."
michael@0 7113 var sublist = [];
michael@0 7114
michael@0 7115 // "Remove the first member of node list and append it to sublist."
michael@0 7116 sublist.push(nodeList.shift());
michael@0 7117
michael@0 7118 // "While the first member of node list is the nextSibling of the
michael@0 7119 // last member of sublist, remove the first member of node list and
michael@0 7120 // append it to sublist."
michael@0 7121 while (nodeList.length
michael@0 7122 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
michael@0 7123 sublist.push(nodeList.shift());
michael@0 7124 }
michael@0 7125
michael@0 7126 // "Indent sublist."
michael@0 7127 indentNodes(sublist);
michael@0 7128 }
michael@0 7129
michael@0 7130 // "Return true."
michael@0 7131 return true;
michael@0 7132 }
michael@0 7133 };
michael@0 7134
michael@0 7135 //@}
michael@0 7136 ///// The insertHorizontalRule command /////
michael@0 7137 //@{
michael@0 7138 commands.inserthorizontalrule = {
michael@0 7139 preservesOverrides: true,
michael@0 7140 action: function() {
michael@0 7141 // "Let start node, start offset, end node, and end offset be the
michael@0 7142 // active range's start and end nodes and offsets."
michael@0 7143 var startNode = getActiveRange().startContainer;
michael@0 7144 var startOffset = getActiveRange().startOffset;
michael@0 7145 var endNode = getActiveRange().endContainer;
michael@0 7146 var endOffset = getActiveRange().endOffset;
michael@0 7147
michael@0 7148 // "While start offset is 0 and start node's parent is not null, set
michael@0 7149 // start offset to start node's index, then set start node to its
michael@0 7150 // parent."
michael@0 7151 while (startOffset == 0
michael@0 7152 && startNode.parentNode) {
michael@0 7153 startOffset = getNodeIndex(startNode);
michael@0 7154 startNode = startNode.parentNode;
michael@0 7155 }
michael@0 7156
michael@0 7157 // "While end offset is end node's length, and end node's parent is not
michael@0 7158 // null, set end offset to one plus end node's index, then set end node
michael@0 7159 // to its parent."
michael@0 7160 while (endOffset == getNodeLength(endNode)
michael@0 7161 && endNode.parentNode) {
michael@0 7162 endOffset = 1 + getNodeIndex(endNode);
michael@0 7163 endNode = endNode.parentNode;
michael@0 7164 }
michael@0 7165
michael@0 7166 // "Call collapse(start node, start offset) on the context object's
michael@0 7167 // Selection."
michael@0 7168 getSelection().collapse(startNode, startOffset);
michael@0 7169 getActiveRange().setStart(startNode, startOffset);
michael@0 7170
michael@0 7171 // "Call extend(end node, end offset) on the context object's
michael@0 7172 // Selection."
michael@0 7173 getSelection().extend(endNode, endOffset);
michael@0 7174 getActiveRange().setEnd(endNode, endOffset);
michael@0 7175
michael@0 7176 // "Delete the selection, with block merging false."
michael@0 7177 deleteSelection({blockMerging: false});
michael@0 7178
michael@0 7179 // "If the active range's start node is neither editable nor an editing
michael@0 7180 // host, return true."
michael@0 7181 if (!isEditable(getActiveRange().startContainer)
michael@0 7182 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7183 return true;
michael@0 7184 }
michael@0 7185
michael@0 7186 // "If the active range's start node is a Text node and its start
michael@0 7187 // offset is zero, call collapse() on the context object's Selection,
michael@0 7188 // with first argument the active range's start node's parent and
michael@0 7189 // second argument the active range's start node's index."
michael@0 7190 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 7191 && getActiveRange().startOffset == 0) {
michael@0 7192 var newNode = getActiveRange().startContainer.parentNode;
michael@0 7193 var newOffset = getNodeIndex(getActiveRange().startContainer);
michael@0 7194 getSelection().collapse(newNode, newOffset);
michael@0 7195 getActiveRange().setStart(newNode, newOffset);
michael@0 7196 getActiveRange().collapse(true);
michael@0 7197 }
michael@0 7198
michael@0 7199 // "If the active range's start node is a Text node and its start
michael@0 7200 // offset is the length of its start node, call collapse() on the
michael@0 7201 // context object's Selection, with first argument the active range's
michael@0 7202 // start node's parent, and the second argument one plus the active
michael@0 7203 // range's start node's index."
michael@0 7204 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 7205 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
michael@0 7206 var newNode = getActiveRange().startContainer.parentNode;
michael@0 7207 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
michael@0 7208 getSelection().collapse(newNode, newOffset);
michael@0 7209 getActiveRange().setStart(newNode, newOffset);
michael@0 7210 getActiveRange().collapse(true);
michael@0 7211 }
michael@0 7212
michael@0 7213 // "Let hr be the result of calling createElement("hr") on the
michael@0 7214 // context object."
michael@0 7215 var hr = document.createElement("hr");
michael@0 7216
michael@0 7217 // "Run insertNode(hr) on the active range."
michael@0 7218 getActiveRange().insertNode(hr);
michael@0 7219
michael@0 7220 // "Fix disallowed ancestors of hr."
michael@0 7221 fixDisallowedAncestors(hr);
michael@0 7222
michael@0 7223 // "Run collapse() on the context object's Selection, with first
michael@0 7224 // argument hr's parent and the second argument equal to one plus hr's
michael@0 7225 // index."
michael@0 7226 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
michael@0 7227 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
michael@0 7228 getActiveRange().collapse(true);
michael@0 7229
michael@0 7230 // "Return true."
michael@0 7231 return true;
michael@0 7232 }
michael@0 7233 };
michael@0 7234
michael@0 7235 //@}
michael@0 7236 ///// The insertHTML command /////
michael@0 7237 //@{
michael@0 7238 commands.inserthtml = {
michael@0 7239 preservesOverrides: true,
michael@0 7240 action: function(value) {
michael@0 7241 // "Delete the selection."
michael@0 7242 deleteSelection();
michael@0 7243
michael@0 7244 // "If the active range's start node is neither editable nor an editing
michael@0 7245 // host, return true."
michael@0 7246 if (!isEditable(getActiveRange().startContainer)
michael@0 7247 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7248 return true;
michael@0 7249 }
michael@0 7250
michael@0 7251 // "Let frag be the result of calling createContextualFragment(value)
michael@0 7252 // on the active range."
michael@0 7253 var frag = getActiveRange().createContextualFragment(value);
michael@0 7254
michael@0 7255 // "Let last child be the lastChild of frag."
michael@0 7256 var lastChild = frag.lastChild;
michael@0 7257
michael@0 7258 // "If last child is null, return true."
michael@0 7259 if (!lastChild) {
michael@0 7260 return true;
michael@0 7261 }
michael@0 7262
michael@0 7263 // "Let descendants be all descendants of frag."
michael@0 7264 var descendants = getDescendants(frag);
michael@0 7265
michael@0 7266 // "If the active range's start node is a block node:"
michael@0 7267 if (isBlockNode(getActiveRange().startContainer)) {
michael@0 7268 // "Let collapsed block props be all editable collapsed block prop
michael@0 7269 // children of the active range's start node that have index
michael@0 7270 // greater than or equal to the active range's start offset."
michael@0 7271 //
michael@0 7272 // "For each node in collapsed block props, remove node from its
michael@0 7273 // parent."
michael@0 7274 [].filter.call(getActiveRange().startContainer.childNodes, function(node) {
michael@0 7275 return isEditable(node)
michael@0 7276 && isCollapsedBlockProp(node)
michael@0 7277 && getNodeIndex(node) >= getActiveRange().startOffset;
michael@0 7278 }).forEach(function(node) {
michael@0 7279 node.parentNode.removeChild(node);
michael@0 7280 });
michael@0 7281 }
michael@0 7282
michael@0 7283 // "Call insertNode(frag) on the active range."
michael@0 7284 getActiveRange().insertNode(frag);
michael@0 7285
michael@0 7286 // "If the active range's start node is a block node with no visible
michael@0 7287 // children, call createElement("br") on the context object and append
michael@0 7288 // the result as the last child of the active range's start node."
michael@0 7289 if (isBlockNode(getActiveRange().startContainer)
michael@0 7290 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
michael@0 7291 getActiveRange().startContainer.appendChild(document.createElement("br"));
michael@0 7292 }
michael@0 7293
michael@0 7294 // "Call collapse() on the context object's Selection, with last
michael@0 7295 // child's parent as the first argument and one plus its index as the
michael@0 7296 // second."
michael@0 7297 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
michael@0 7298 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
michael@0 7299
michael@0 7300 // "Fix disallowed ancestors of each member of descendants."
michael@0 7301 for (var i = 0; i < descendants.length; i++) {
michael@0 7302 fixDisallowedAncestors(descendants[i]);
michael@0 7303 }
michael@0 7304
michael@0 7305 // "Return true."
michael@0 7306 return true;
michael@0 7307 }
michael@0 7308 };
michael@0 7309
michael@0 7310 //@}
michael@0 7311 ///// The insertImage command /////
michael@0 7312 //@{
michael@0 7313 commands.insertimage = {
michael@0 7314 preservesOverrides: true,
michael@0 7315 action: function(value) {
michael@0 7316 // "If value is the empty string, return false."
michael@0 7317 if (value === "") {
michael@0 7318 return false;
michael@0 7319 }
michael@0 7320
michael@0 7321 // "Delete the selection, with strip wrappers false."
michael@0 7322 deleteSelection({stripWrappers: false});
michael@0 7323
michael@0 7324 // "Let range be the active range."
michael@0 7325 var range = getActiveRange();
michael@0 7326
michael@0 7327 // "If the active range's start node is neither editable nor an editing
michael@0 7328 // host, return true."
michael@0 7329 if (!isEditable(getActiveRange().startContainer)
michael@0 7330 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7331 return true;
michael@0 7332 }
michael@0 7333
michael@0 7334 // "If range's start node is a block node whose sole child is a br, and
michael@0 7335 // its start offset is 0, remove its start node's child from it."
michael@0 7336 if (isBlockNode(range.startContainer)
michael@0 7337 && range.startContainer.childNodes.length == 1
michael@0 7338 && isHtmlElement(range.startContainer.firstChild, "br")
michael@0 7339 && range.startOffset == 0) {
michael@0 7340 range.startContainer.removeChild(range.startContainer.firstChild);
michael@0 7341 }
michael@0 7342
michael@0 7343 // "Let img be the result of calling createElement("img") on the
michael@0 7344 // context object."
michael@0 7345 var img = document.createElement("img");
michael@0 7346
michael@0 7347 // "Run setAttribute("src", value) on img."
michael@0 7348 img.setAttribute("src", value);
michael@0 7349
michael@0 7350 // "Run insertNode(img) on the range."
michael@0 7351 range.insertNode(img);
michael@0 7352
michael@0 7353 // "Run collapse() on the Selection, with first argument equal to the
michael@0 7354 // parent of img and the second argument equal to one plus the index of
michael@0 7355 // img."
michael@0 7356 //
michael@0 7357 // Not everyone actually supports collapse(), so we do it manually
michael@0 7358 // instead. Also, we need to modify the actual range we're given as
michael@0 7359 // well, for the sake of autoimplementation.html's range-filling-in.
michael@0 7360 range.setStart(img.parentNode, 1 + getNodeIndex(img));
michael@0 7361 range.setEnd(img.parentNode, 1 + getNodeIndex(img));
michael@0 7362 getSelection().removeAllRanges();
michael@0 7363 getSelection().addRange(range);
michael@0 7364
michael@0 7365 // IE adds width and height attributes for some reason, so remove those
michael@0 7366 // to actually do what the spec says.
michael@0 7367 img.removeAttribute("width");
michael@0 7368 img.removeAttribute("height");
michael@0 7369
michael@0 7370 // "Return true."
michael@0 7371 return true;
michael@0 7372 }
michael@0 7373 };
michael@0 7374
michael@0 7375 //@}
michael@0 7376 ///// The insertLineBreak command /////
michael@0 7377 //@{
michael@0 7378 commands.insertlinebreak = {
michael@0 7379 preservesOverrides: true,
michael@0 7380 action: function(value) {
michael@0 7381 // "Delete the selection, with strip wrappers false."
michael@0 7382 deleteSelection({stripWrappers: false});
michael@0 7383
michael@0 7384 // "If the active range's start node is neither editable nor an editing
michael@0 7385 // host, return true."
michael@0 7386 if (!isEditable(getActiveRange().startContainer)
michael@0 7387 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7388 return true;
michael@0 7389 }
michael@0 7390
michael@0 7391 // "If the active range's start node is an Element, and "br" is not an
michael@0 7392 // allowed child of it, return true."
michael@0 7393 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
michael@0 7394 && !isAllowedChild("br", getActiveRange().startContainer)) {
michael@0 7395 return true;
michael@0 7396 }
michael@0 7397
michael@0 7398 // "If the active range's start node is not an Element, and "br" is not
michael@0 7399 // an allowed child of the active range's start node's parent, return
michael@0 7400 // true."
michael@0 7401 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
michael@0 7402 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
michael@0 7403 return true;
michael@0 7404 }
michael@0 7405
michael@0 7406 // "If the active range's start node is a Text node and its start
michael@0 7407 // offset is zero, call collapse() on the context object's Selection,
michael@0 7408 // with first argument equal to the active range's start node's parent
michael@0 7409 // and second argument equal to the active range's start node's index."
michael@0 7410 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 7411 && getActiveRange().startOffset == 0) {
michael@0 7412 var newNode = getActiveRange().startContainer.parentNode;
michael@0 7413 var newOffset = getNodeIndex(getActiveRange().startContainer);
michael@0 7414 getSelection().collapse(newNode, newOffset);
michael@0 7415 getActiveRange().setStart(newNode, newOffset);
michael@0 7416 getActiveRange().setEnd(newNode, newOffset);
michael@0 7417 }
michael@0 7418
michael@0 7419 // "If the active range's start node is a Text node and its start
michael@0 7420 // offset is the length of its start node, call collapse() on the
michael@0 7421 // context object's Selection, with first argument equal to the active
michael@0 7422 // range's start node's parent and second argument equal to one plus
michael@0 7423 // the active range's start node's index."
michael@0 7424 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
michael@0 7425 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
michael@0 7426 var newNode = getActiveRange().startContainer.parentNode;
michael@0 7427 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
michael@0 7428 getSelection().collapse(newNode, newOffset);
michael@0 7429 getActiveRange().setStart(newNode, newOffset);
michael@0 7430 getActiveRange().setEnd(newNode, newOffset);
michael@0 7431 }
michael@0 7432
michael@0 7433 // "Let br be the result of calling createElement("br") on the context
michael@0 7434 // object."
michael@0 7435 var br = document.createElement("br");
michael@0 7436
michael@0 7437 // "Call insertNode(br) on the active range."
michael@0 7438 getActiveRange().insertNode(br);
michael@0 7439
michael@0 7440 // "Call collapse() on the context object's Selection, with br's parent
michael@0 7441 // as the first argument and one plus br's index as the second
michael@0 7442 // argument."
michael@0 7443 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
michael@0 7444 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
michael@0 7445 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
michael@0 7446
michael@0 7447 // "If br is a collapsed line break, call createElement("br") on the
michael@0 7448 // context object and let extra br be the result, then call
michael@0 7449 // insertNode(extra br) on the active range."
michael@0 7450 if (isCollapsedLineBreak(br)) {
michael@0 7451 getActiveRange().insertNode(document.createElement("br"));
michael@0 7452
michael@0 7453 // Compensate for nonstandard implementations of insertNode
michael@0 7454 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
michael@0 7455 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
michael@0 7456 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
michael@0 7457 }
michael@0 7458
michael@0 7459 // "Return true."
michael@0 7460 return true;
michael@0 7461 }
michael@0 7462 };
michael@0 7463
michael@0 7464 //@}
michael@0 7465 ///// The insertOrderedList command /////
michael@0 7466 //@{
michael@0 7467 commands.insertorderedlist = {
michael@0 7468 preservesOverrides: true,
michael@0 7469 // "Toggle lists with tag name "ol", then return true."
michael@0 7470 action: function() { toggleLists("ol"); return true },
michael@0 7471 // "True if the selection's list state is "mixed" or "mixed ol", false
michael@0 7472 // otherwise."
michael@0 7473 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
michael@0 7474 // "True if the selection's list state is "ol", false otherwise."
michael@0 7475 state: function() { return getSelectionListState() == "ol" },
michael@0 7476 };
michael@0 7477
michael@0 7478 //@}
michael@0 7479 ///// The insertParagraph command /////
michael@0 7480 //@{
michael@0 7481 commands.insertparagraph = {
michael@0 7482 preservesOverrides: true,
michael@0 7483 action: function() {
michael@0 7484 // "Delete the selection."
michael@0 7485 deleteSelection();
michael@0 7486
michael@0 7487 // "If the active range's start node is neither editable nor an editing
michael@0 7488 // host, return true."
michael@0 7489 if (!isEditable(getActiveRange().startContainer)
michael@0 7490 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7491 return true;
michael@0 7492 }
michael@0 7493
michael@0 7494 // "Let node and offset be the active range's start node and offset."
michael@0 7495 var node = getActiveRange().startContainer;
michael@0 7496 var offset = getActiveRange().startOffset;
michael@0 7497
michael@0 7498 // "If node is a Text node, and offset is neither 0 nor the length of
michael@0 7499 // node, call splitText(offset) on node."
michael@0 7500 if (node.nodeType == Node.TEXT_NODE
michael@0 7501 && offset != 0
michael@0 7502 && offset != getNodeLength(node)) {
michael@0 7503 node.splitText(offset);
michael@0 7504 }
michael@0 7505
michael@0 7506 // "If node is a Text node and offset is its length, set offset to one
michael@0 7507 // plus the index of node, then set node to its parent."
michael@0 7508 if (node.nodeType == Node.TEXT_NODE
michael@0 7509 && offset == getNodeLength(node)) {
michael@0 7510 offset = 1 + getNodeIndex(node);
michael@0 7511 node = node.parentNode;
michael@0 7512 }
michael@0 7513
michael@0 7514 // "If node is a Text or Comment node, set offset to the index of node,
michael@0 7515 // then set node to its parent."
michael@0 7516 if (node.nodeType == Node.TEXT_NODE
michael@0 7517 || node.nodeType == Node.COMMENT_NODE) {
michael@0 7518 offset = getNodeIndex(node);
michael@0 7519 node = node.parentNode;
michael@0 7520 }
michael@0 7521
michael@0 7522 // "Call collapse(node, offset) on the context object's Selection."
michael@0 7523 getSelection().collapse(node, offset);
michael@0 7524 getActiveRange().setStart(node, offset);
michael@0 7525 getActiveRange().setEnd(node, offset);
michael@0 7526
michael@0 7527 // "Let container equal node."
michael@0 7528 var container = node;
michael@0 7529
michael@0 7530 // "While container is not a single-line container, and container's
michael@0 7531 // parent is editable and in the same editing host as node, set
michael@0 7532 // container to its parent."
michael@0 7533 while (!isSingleLineContainer(container)
michael@0 7534 && isEditable(container.parentNode)
michael@0 7535 && inSameEditingHost(node, container.parentNode)) {
michael@0 7536 container = container.parentNode;
michael@0 7537 }
michael@0 7538
michael@0 7539 // "If container is an editable single-line container in the same
michael@0 7540 // editing host as node, and its local name is "p" or "div":"
michael@0 7541 if (isEditable(container)
michael@0 7542 && isSingleLineContainer(container)
michael@0 7543 && inSameEditingHost(node, container.parentNode)
michael@0 7544 && (container.tagName == "P" || container.tagName == "DIV")) {
michael@0 7545 // "Let outer container equal container."
michael@0 7546 var outerContainer = container;
michael@0 7547
michael@0 7548 // "While outer container is not a dd or dt or li, and outer
michael@0 7549 // container's parent is editable, set outer container to its
michael@0 7550 // parent."
michael@0 7551 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
michael@0 7552 && isEditable(outerContainer.parentNode)) {
michael@0 7553 outerContainer = outerContainer.parentNode;
michael@0 7554 }
michael@0 7555
michael@0 7556 // "If outer container is a dd or dt or li, set container to outer
michael@0 7557 // container."
michael@0 7558 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
michael@0 7559 container = outerContainer;
michael@0 7560 }
michael@0 7561 }
michael@0 7562
michael@0 7563 // "If container is not editable or not in the same editing host as
michael@0 7564 // node or is not a single-line container:"
michael@0 7565 if (!isEditable(container)
michael@0 7566 || !inSameEditingHost(container, node)
michael@0 7567 || !isSingleLineContainer(container)) {
michael@0 7568 // "Let tag be the default single-line container name."
michael@0 7569 var tag = defaultSingleLineContainerName;
michael@0 7570
michael@0 7571 // "Block-extend the active range, and let new range be the
michael@0 7572 // result."
michael@0 7573 var newRange = blockExtend(getActiveRange());
michael@0 7574
michael@0 7575 // "Let node list be a list of nodes, initially empty."
michael@0 7576 //
michael@0 7577 // "Append to node list the first node in tree order that is
michael@0 7578 // contained in new range and is an allowed child of "p", if any."
michael@0 7579 var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
michael@0 7580 .slice(0, 1);
michael@0 7581
michael@0 7582 // "If node list is empty:"
michael@0 7583 if (!nodeList.length) {
michael@0 7584 // "If tag is not an allowed child of the active range's start
michael@0 7585 // node, return true."
michael@0 7586 if (!isAllowedChild(tag, getActiveRange().startContainer)) {
michael@0 7587 return true;
michael@0 7588 }
michael@0 7589
michael@0 7590 // "Set container to the result of calling createElement(tag)
michael@0 7591 // on the context object."
michael@0 7592 container = document.createElement(tag);
michael@0 7593
michael@0 7594 // "Call insertNode(container) on the active range."
michael@0 7595 getActiveRange().insertNode(container);
michael@0 7596
michael@0 7597 // "Call createElement("br") on the context object, and append
michael@0 7598 // the result as the last child of container."
michael@0 7599 container.appendChild(document.createElement("br"));
michael@0 7600
michael@0 7601 // "Call collapse(container, 0) on the context object's
michael@0 7602 // Selection."
michael@0 7603 getSelection().collapse(container, 0);
michael@0 7604 getActiveRange().setStart(container, 0);
michael@0 7605 getActiveRange().setEnd(container, 0);
michael@0 7606
michael@0 7607 // "Return true."
michael@0 7608 return true;
michael@0 7609 }
michael@0 7610
michael@0 7611 // "While the nextSibling of the last member of node list is not
michael@0 7612 // null and is an allowed child of "p", append it to node list."
michael@0 7613 while (nodeList[nodeList.length - 1].nextSibling
michael@0 7614 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
michael@0 7615 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
michael@0 7616 }
michael@0 7617
michael@0 7618 // "Wrap node list, with sibling criteria returning false and new
michael@0 7619 // parent instructions returning the result of calling
michael@0 7620 // createElement(tag) on the context object. Set container to the
michael@0 7621 // result."
michael@0 7622 container = wrap(nodeList,
michael@0 7623 function() { return false },
michael@0 7624 function() { return document.createElement(tag) }
michael@0 7625 );
michael@0 7626 }
michael@0 7627
michael@0 7628 // "If container's local name is "address", "listing", or "pre":"
michael@0 7629 if (container.tagName == "ADDRESS"
michael@0 7630 || container.tagName == "LISTING"
michael@0 7631 || container.tagName == "PRE") {
michael@0 7632 // "Let br be the result of calling createElement("br") on the
michael@0 7633 // context object."
michael@0 7634 var br = document.createElement("br");
michael@0 7635
michael@0 7636 // "Call insertNode(br) on the active range."
michael@0 7637 getActiveRange().insertNode(br);
michael@0 7638
michael@0 7639 // "Call collapse(node, offset + 1) on the context object's
michael@0 7640 // Selection."
michael@0 7641 getSelection().collapse(node, offset + 1);
michael@0 7642 getActiveRange().setStart(node, offset + 1);
michael@0 7643 getActiveRange().setEnd(node, offset + 1);
michael@0 7644
michael@0 7645 // "If br is the last descendant of container, let br be the result
michael@0 7646 // of calling createElement("br") on the context object, then call
michael@0 7647 // insertNode(br) on the active range."
michael@0 7648 //
michael@0 7649 // Work around browser bugs: some browsers select the
michael@0 7650 // newly-inserted node, not per spec.
michael@0 7651 if (!isDescendant(nextNode(br), container)) {
michael@0 7652 getActiveRange().insertNode(document.createElement("br"));
michael@0 7653 getSelection().collapse(node, offset + 1);
michael@0 7654 getActiveRange().setEnd(node, offset + 1);
michael@0 7655 }
michael@0 7656
michael@0 7657 // "Return true."
michael@0 7658 return true;
michael@0 7659 }
michael@0 7660
michael@0 7661 // "If container's local name is "li", "dt", or "dd"; and either it has
michael@0 7662 // no children or it has a single child and that child is a br:"
michael@0 7663 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
michael@0 7664 && (!container.hasChildNodes()
michael@0 7665 || (container.childNodes.length == 1
michael@0 7666 && isHtmlElement(container.firstChild, "br")))) {
michael@0 7667 // "Split the parent of the one-node list consisting of container."
michael@0 7668 splitParent([container]);
michael@0 7669
michael@0 7670 // "If container has no children, call createElement("br") on the
michael@0 7671 // context object and append the result as the last child of
michael@0 7672 // container."
michael@0 7673 if (!container.hasChildNodes()) {
michael@0 7674 container.appendChild(document.createElement("br"));
michael@0 7675 }
michael@0 7676
michael@0 7677 // "If container is a dd or dt, and it is not an allowed child of
michael@0 7678 // any of its ancestors in the same editing host, set the tag name
michael@0 7679 // of container to the default single-line container name and let
michael@0 7680 // container be the result."
michael@0 7681 if (isHtmlElement(container, ["dd", "dt"])
michael@0 7682 && getAncestors(container).every(function(ancestor) {
michael@0 7683 return !inSameEditingHost(container, ancestor)
michael@0 7684 || !isAllowedChild(container, ancestor)
michael@0 7685 })) {
michael@0 7686 container = setTagName(container, defaultSingleLineContainerName);
michael@0 7687 }
michael@0 7688
michael@0 7689 // "Fix disallowed ancestors of container."
michael@0 7690 fixDisallowedAncestors(container);
michael@0 7691
michael@0 7692 // "Return true."
michael@0 7693 return true;
michael@0 7694 }
michael@0 7695
michael@0 7696 // "Let new line range be a new range whose start is the same as
michael@0 7697 // the active range's, and whose end is (container, length of
michael@0 7698 // container)."
michael@0 7699 var newLineRange = document.createRange();
michael@0 7700 newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 7701 newLineRange.setEnd(container, getNodeLength(container));
michael@0 7702
michael@0 7703 // "While new line range's start offset is zero and its start node is
michael@0 7704 // not a prohibited paragraph child, set its start to (parent of start
michael@0 7705 // node, index of start node)."
michael@0 7706 while (newLineRange.startOffset == 0
michael@0 7707 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
michael@0 7708 newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
michael@0 7709 }
michael@0 7710
michael@0 7711 // "While new line range's start offset is the length of its start node
michael@0 7712 // and its start node is not a prohibited paragraph child, set its
michael@0 7713 // start to (parent of start node, 1 + index of start node)."
michael@0 7714 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
michael@0 7715 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
michael@0 7716 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
michael@0 7717 }
michael@0 7718
michael@0 7719 // "Let end of line be true if new line range contains either nothing
michael@0 7720 // or a single br, and false otherwise."
michael@0 7721 var containedInNewLineRange = getContainedNodes(newLineRange);
michael@0 7722 var endOfLine = !containedInNewLineRange.length
michael@0 7723 || (containedInNewLineRange.length == 1
michael@0 7724 && isHtmlElement(containedInNewLineRange[0], "br"));
michael@0 7725
michael@0 7726 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
michael@0 7727 // "h6", and end of line is true, let new container name be the default
michael@0 7728 // single-line container name."
michael@0 7729 var newContainerName;
michael@0 7730 if (/^H[1-6]$/.test(container.tagName)
michael@0 7731 && endOfLine) {
michael@0 7732 newContainerName = defaultSingleLineContainerName;
michael@0 7733
michael@0 7734 // "Otherwise, if the local name of container is "dt" and end of line
michael@0 7735 // is true, let new container name be "dd"."
michael@0 7736 } else if (container.tagName == "DT"
michael@0 7737 && endOfLine) {
michael@0 7738 newContainerName = "dd";
michael@0 7739
michael@0 7740 // "Otherwise, if the local name of container is "dd" and end of line
michael@0 7741 // is true, let new container name be "dt"."
michael@0 7742 } else if (container.tagName == "DD"
michael@0 7743 && endOfLine) {
michael@0 7744 newContainerName = "dt";
michael@0 7745
michael@0 7746 // "Otherwise, let new container name be the local name of container."
michael@0 7747 } else {
michael@0 7748 newContainerName = container.tagName.toLowerCase();
michael@0 7749 }
michael@0 7750
michael@0 7751 // "Let new container be the result of calling createElement(new
michael@0 7752 // container name) on the context object."
michael@0 7753 var newContainer = document.createElement(newContainerName);
michael@0 7754
michael@0 7755 // "Copy all attributes of container to new container."
michael@0 7756 for (var i = 0; i < container.attributes.length; i++) {
michael@0 7757 newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value);
michael@0 7758 }
michael@0 7759
michael@0 7760 // "If new container has an id attribute, unset it."
michael@0 7761 newContainer.removeAttribute("id");
michael@0 7762
michael@0 7763 // "Insert new container into the parent of container immediately after
michael@0 7764 // container."
michael@0 7765 container.parentNode.insertBefore(newContainer, container.nextSibling);
michael@0 7766
michael@0 7767 // "Let contained nodes be all nodes contained in new line range."
michael@0 7768 var containedNodes = getAllContainedNodes(newLineRange);
michael@0 7769
michael@0 7770 // "Let frag be the result of calling extractContents() on new line
michael@0 7771 // range."
michael@0 7772 var frag = newLineRange.extractContents();
michael@0 7773
michael@0 7774 // "Unset the id attribute (if any) of each Element descendant of frag
michael@0 7775 // that is not in contained nodes."
michael@0 7776 var descendants = getDescendants(frag);
michael@0 7777 for (var i = 0; i < descendants.length; i++) {
michael@0 7778 if (descendants[i].nodeType == Node.ELEMENT_NODE
michael@0 7779 && containedNodes.indexOf(descendants[i]) == -1) {
michael@0 7780 descendants[i].removeAttribute("id");
michael@0 7781 }
michael@0 7782 }
michael@0 7783
michael@0 7784 // "Call appendChild(frag) on new container."
michael@0 7785 newContainer.appendChild(frag);
michael@0 7786
michael@0 7787 // "While container's lastChild is a prohibited paragraph child, set
michael@0 7788 // container to its lastChild."
michael@0 7789 while (isProhibitedParagraphChild(container.lastChild)) {
michael@0 7790 container = container.lastChild;
michael@0 7791 }
michael@0 7792
michael@0 7793 // "While new container's lastChild is a prohibited paragraph child,
michael@0 7794 // set new container to its lastChild."
michael@0 7795 while (isProhibitedParagraphChild(newContainer.lastChild)) {
michael@0 7796 newContainer = newContainer.lastChild;
michael@0 7797 }
michael@0 7798
michael@0 7799 // "If container has no visible children, call createElement("br") on
michael@0 7800 // the context object, and append the result as the last child of
michael@0 7801 // container."
michael@0 7802 if (![].some.call(container.childNodes, isVisible)) {
michael@0 7803 container.appendChild(document.createElement("br"));
michael@0 7804 }
michael@0 7805
michael@0 7806 // "If new container has no visible children, call createElement("br")
michael@0 7807 // on the context object, and append the result as the last child of
michael@0 7808 // new container."
michael@0 7809 if (![].some.call(newContainer.childNodes, isVisible)) {
michael@0 7810 newContainer.appendChild(document.createElement("br"));
michael@0 7811 }
michael@0 7812
michael@0 7813 // "Call collapse(new container, 0) on the context object's Selection."
michael@0 7814 getSelection().collapse(newContainer, 0);
michael@0 7815 getActiveRange().setStart(newContainer, 0);
michael@0 7816 getActiveRange().setEnd(newContainer, 0);
michael@0 7817
michael@0 7818 // "Return true."
michael@0 7819 return true;
michael@0 7820 }
michael@0 7821 };
michael@0 7822
michael@0 7823 //@}
michael@0 7824 ///// The insertText command /////
michael@0 7825 //@{
michael@0 7826 commands.inserttext = {
michael@0 7827 action: function(value) {
michael@0 7828 // "Delete the selection, with strip wrappers false."
michael@0 7829 deleteSelection({stripWrappers: false});
michael@0 7830
michael@0 7831 // "If the active range's start node is neither editable nor an editing
michael@0 7832 // host, return true."
michael@0 7833 if (!isEditable(getActiveRange().startContainer)
michael@0 7834 && !isEditingHost(getActiveRange().startContainer)) {
michael@0 7835 return true;
michael@0 7836 }
michael@0 7837
michael@0 7838 // "If value's length is greater than one:"
michael@0 7839 if (value.length > 1) {
michael@0 7840 // "For each element el in value, take the action for the
michael@0 7841 // insertText command, with value equal to el."
michael@0 7842 for (var i = 0; i < value.length; i++) {
michael@0 7843 commands.inserttext.action(value[i]);
michael@0 7844 }
michael@0 7845
michael@0 7846 // "Return true."
michael@0 7847 return true;
michael@0 7848 }
michael@0 7849
michael@0 7850 // "If value is the empty string, return true."
michael@0 7851 if (value == "") {
michael@0 7852 return true;
michael@0 7853 }
michael@0 7854
michael@0 7855 // "If value is a newline (U+00A0), take the action for the
michael@0 7856 // insertParagraph command and return true."
michael@0 7857 if (value == "\n") {
michael@0 7858 commands.insertparagraph.action();
michael@0 7859 return true;
michael@0 7860 }
michael@0 7861
michael@0 7862 // "Let node and offset be the active range's start node and offset."
michael@0 7863 var node = getActiveRange().startContainer;
michael@0 7864 var offset = getActiveRange().startOffset;
michael@0 7865
michael@0 7866 // "If node has a child whose index is offset − 1, and that child is a
michael@0 7867 // Text node, set node to that child, then set offset to node's
michael@0 7868 // length."
michael@0 7869 if (0 <= offset - 1
michael@0 7870 && offset - 1 < node.childNodes.length
michael@0 7871 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
michael@0 7872 node = node.childNodes[offset - 1];
michael@0 7873 offset = getNodeLength(node);
michael@0 7874 }
michael@0 7875
michael@0 7876 // "If node has a child whose index is offset, and that child is a Text
michael@0 7877 // node, set node to that child, then set offset to zero."
michael@0 7878 if (0 <= offset
michael@0 7879 && offset < node.childNodes.length
michael@0 7880 && node.childNodes[offset].nodeType == Node.TEXT_NODE) {
michael@0 7881 node = node.childNodes[offset];
michael@0 7882 offset = 0;
michael@0 7883 }
michael@0 7884
michael@0 7885 // "Record current overrides, and let overrides be the result."
michael@0 7886 var overrides = recordCurrentOverrides();
michael@0 7887
michael@0 7888 // "Call collapse(node, offset) on the context object's Selection."
michael@0 7889 getSelection().collapse(node, offset);
michael@0 7890 getActiveRange().setStart(node, offset);
michael@0 7891 getActiveRange().setEnd(node, offset);
michael@0 7892
michael@0 7893 // "Canonicalize whitespace at (node, offset)."
michael@0 7894 canonicalizeWhitespace(node, offset);
michael@0 7895
michael@0 7896 // "Let (node, offset) be the active range's start."
michael@0 7897 node = getActiveRange().startContainer;
michael@0 7898 offset = getActiveRange().startOffset;
michael@0 7899
michael@0 7900 // "If node is a Text node:"
michael@0 7901 if (node.nodeType == Node.TEXT_NODE) {
michael@0 7902 // "Call insertData(offset, value) on node."
michael@0 7903 node.insertData(offset, value);
michael@0 7904
michael@0 7905 // "Call collapse(node, offset) on the context object's Selection."
michael@0 7906 getSelection().collapse(node, offset);
michael@0 7907 getActiveRange().setStart(node, offset);
michael@0 7908
michael@0 7909 // "Call extend(node, offset + 1) on the context object's
michael@0 7910 // Selection."
michael@0 7911 //
michael@0 7912 // Work around WebKit bug: the extend() can throw if the text we're
michael@0 7913 // adding is trailing whitespace.
michael@0 7914 try { getSelection().extend(node, offset + 1); } catch(e) {}
michael@0 7915 getActiveRange().setEnd(node, offset + 1);
michael@0 7916
michael@0 7917 // "Otherwise:"
michael@0 7918 } else {
michael@0 7919 // "If node has only one child, which is a collapsed line break,
michael@0 7920 // remove its child from it."
michael@0 7921 //
michael@0 7922 // FIXME: IE incorrectly returns false here instead of true
michael@0 7923 // sometimes?
michael@0 7924 if (node.childNodes.length == 1
michael@0 7925 && isCollapsedLineBreak(node.firstChild)) {
michael@0 7926 node.removeChild(node.firstChild);
michael@0 7927 }
michael@0 7928
michael@0 7929 // "Let text be the result of calling createTextNode(value) on the
michael@0 7930 // context object."
michael@0 7931 var text = document.createTextNode(value);
michael@0 7932
michael@0 7933 // "Call insertNode(text) on the active range."
michael@0 7934 getActiveRange().insertNode(text);
michael@0 7935
michael@0 7936 // "Call collapse(text, 0) on the context object's Selection."
michael@0 7937 getSelection().collapse(text, 0);
michael@0 7938 getActiveRange().setStart(text, 0);
michael@0 7939
michael@0 7940 // "Call extend(text, 1) on the context object's Selection."
michael@0 7941 getSelection().extend(text, 1);
michael@0 7942 getActiveRange().setEnd(text, 1);
michael@0 7943 }
michael@0 7944
michael@0 7945 // "Restore states and values from overrides."
michael@0 7946 restoreStatesAndValues(overrides);
michael@0 7947
michael@0 7948 // "Canonicalize whitespace at the active range's start, with fix
michael@0 7949 // collapsed space false."
michael@0 7950 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
michael@0 7951
michael@0 7952 // "Canonicalize whitespace at the active range's end, with fix
michael@0 7953 // collapsed space false."
michael@0 7954 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
michael@0 7955
michael@0 7956 // "If value is a space character, autolink the active range's start."
michael@0 7957 if (/^[ \t\n\f\r]$/.test(value)) {
michael@0 7958 autolink(getActiveRange().startContainer, getActiveRange().startOffset);
michael@0 7959 }
michael@0 7960
michael@0 7961 // "Call collapseToEnd() on the context object's Selection."
michael@0 7962 //
michael@0 7963 // Work around WebKit bug: sometimes it blows up the selection and
michael@0 7964 // throws, which we don't want.
michael@0 7965 try { getSelection().collapseToEnd(); } catch(e) {}
michael@0 7966 getActiveRange().collapse(false);
michael@0 7967
michael@0 7968 // "Return true."
michael@0 7969 return true;
michael@0 7970 }
michael@0 7971 };
michael@0 7972
michael@0 7973 //@}
michael@0 7974 ///// The insertUnorderedList command /////
michael@0 7975 //@{
michael@0 7976 commands.insertunorderedlist = {
michael@0 7977 preservesOverrides: true,
michael@0 7978 // "Toggle lists with tag name "ul", then return true."
michael@0 7979 action: function() { toggleLists("ul"); return true },
michael@0 7980 // "True if the selection's list state is "mixed" or "mixed ul", false
michael@0 7981 // otherwise."
michael@0 7982 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
michael@0 7983 // "True if the selection's list state is "ul", false otherwise."
michael@0 7984 state: function() { return getSelectionListState() == "ul" },
michael@0 7985 };
michael@0 7986
michael@0 7987 //@}
michael@0 7988 ///// The justifyCenter command /////
michael@0 7989 //@{
michael@0 7990 commands.justifycenter = {
michael@0 7991 preservesOverrides: true,
michael@0 7992 // "Justify the selection with alignment "center", then return true."
michael@0 7993 action: function() { justifySelection("center"); return true },
michael@0 7994 indeterm: function() {
michael@0 7995 // "Return false if the active range is null. Otherwise, block-extend
michael@0 7996 // the active range. Return true if among visible editable nodes that
michael@0 7997 // are contained in the result and have no children, at least one has
michael@0 7998 // alignment value "center" and at least one does not. Otherwise return
michael@0 7999 // false."
michael@0 8000 if (!getActiveRange()) {
michael@0 8001 return false;
michael@0 8002 }
michael@0 8003 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8004 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8005 });
michael@0 8006 return nodes.some(function(node) { return getAlignmentValue(node) == "center" })
michael@0 8007 && nodes.some(function(node) { return getAlignmentValue(node) != "center" });
michael@0 8008 }, state: function() {
michael@0 8009 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8010 // the active range. Return true if there is at least one visible
michael@0 8011 // editable node that is contained in the result and has no children,
michael@0 8012 // and all such nodes have alignment value "center". Otherwise return
michael@0 8013 // false."
michael@0 8014 if (!getActiveRange()) {
michael@0 8015 return false;
michael@0 8016 }
michael@0 8017 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8018 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8019 });
michael@0 8020 return nodes.length
michael@0 8021 && nodes.every(function(node) { return getAlignmentValue(node) == "center" });
michael@0 8022 }, value: function() {
michael@0 8023 // "Return the empty string if the active range is null. Otherwise,
michael@0 8024 // block-extend the active range, and return the alignment value of the
michael@0 8025 // first visible editable node that is contained in the result and has
michael@0 8026 // no children. If there is no such node, return "left"."
michael@0 8027 if (!getActiveRange()) {
michael@0 8028 return "";
michael@0 8029 }
michael@0 8030 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8031 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8032 });
michael@0 8033 if (nodes.length) {
michael@0 8034 return getAlignmentValue(nodes[0]);
michael@0 8035 } else {
michael@0 8036 return "left";
michael@0 8037 }
michael@0 8038 },
michael@0 8039 };
michael@0 8040
michael@0 8041 //@}
michael@0 8042 ///// The justifyFull command /////
michael@0 8043 //@{
michael@0 8044 commands.justifyfull = {
michael@0 8045 preservesOverrides: true,
michael@0 8046 // "Justify the selection with alignment "justify", then return true."
michael@0 8047 action: function() { justifySelection("justify"); return true },
michael@0 8048 indeterm: function() {
michael@0 8049 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8050 // the active range. Return true if among visible editable nodes that
michael@0 8051 // are contained in the result and have no children, at least one has
michael@0 8052 // alignment value "justify" and at least one does not. Otherwise
michael@0 8053 // return false."
michael@0 8054 if (!getActiveRange()) {
michael@0 8055 return false;
michael@0 8056 }
michael@0 8057 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8058 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8059 });
michael@0 8060 return nodes.some(function(node) { return getAlignmentValue(node) == "justify" })
michael@0 8061 && nodes.some(function(node) { return getAlignmentValue(node) != "justify" });
michael@0 8062 }, state: function() {
michael@0 8063 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8064 // the active range. Return true if there is at least one visible
michael@0 8065 // editable node that is contained in the result and has no children,
michael@0 8066 // and all such nodes have alignment value "justify". Otherwise return
michael@0 8067 // false."
michael@0 8068 if (!getActiveRange()) {
michael@0 8069 return false;
michael@0 8070 }
michael@0 8071 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8072 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8073 });
michael@0 8074 return nodes.length
michael@0 8075 && nodes.every(function(node) { return getAlignmentValue(node) == "justify" });
michael@0 8076 }, value: function() {
michael@0 8077 // "Return the empty string if the active range is null. Otherwise,
michael@0 8078 // block-extend the active range, and return the alignment value of the
michael@0 8079 // first visible editable node that is contained in the result and has
michael@0 8080 // no children. If there is no such node, return "left"."
michael@0 8081 if (!getActiveRange()) {
michael@0 8082 return "";
michael@0 8083 }
michael@0 8084 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8085 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8086 });
michael@0 8087 if (nodes.length) {
michael@0 8088 return getAlignmentValue(nodes[0]);
michael@0 8089 } else {
michael@0 8090 return "left";
michael@0 8091 }
michael@0 8092 },
michael@0 8093 };
michael@0 8094
michael@0 8095 //@}
michael@0 8096 ///// The justifyLeft command /////
michael@0 8097 //@{
michael@0 8098 commands.justifyleft = {
michael@0 8099 preservesOverrides: true,
michael@0 8100 // "Justify the selection with alignment "left", then return true."
michael@0 8101 action: function() { justifySelection("left"); return true },
michael@0 8102 indeterm: function() {
michael@0 8103 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8104 // the active range. Return true if among visible editable nodes that
michael@0 8105 // are contained in the result and have no children, at least one has
michael@0 8106 // alignment value "left" and at least one does not. Otherwise return
michael@0 8107 // false."
michael@0 8108 if (!getActiveRange()) {
michael@0 8109 return false;
michael@0 8110 }
michael@0 8111 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8112 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8113 });
michael@0 8114 return nodes.some(function(node) { return getAlignmentValue(node) == "left" })
michael@0 8115 && nodes.some(function(node) { return getAlignmentValue(node) != "left" });
michael@0 8116 }, state: function() {
michael@0 8117 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8118 // the active range. Return true if there is at least one visible
michael@0 8119 // editable node that is contained in the result and has no children,
michael@0 8120 // and all such nodes have alignment value "left". Otherwise return
michael@0 8121 // false."
michael@0 8122 if (!getActiveRange()) {
michael@0 8123 return false;
michael@0 8124 }
michael@0 8125 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8126 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8127 });
michael@0 8128 return nodes.length
michael@0 8129 && nodes.every(function(node) { return getAlignmentValue(node) == "left" });
michael@0 8130 }, value: function() {
michael@0 8131 // "Return the empty string if the active range is null. Otherwise,
michael@0 8132 // block-extend the active range, and return the alignment value of the
michael@0 8133 // first visible editable node that is contained in the result and has
michael@0 8134 // no children. If there is no such node, return "left"."
michael@0 8135 if (!getActiveRange()) {
michael@0 8136 return "";
michael@0 8137 }
michael@0 8138 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8139 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8140 });
michael@0 8141 if (nodes.length) {
michael@0 8142 return getAlignmentValue(nodes[0]);
michael@0 8143 } else {
michael@0 8144 return "left";
michael@0 8145 }
michael@0 8146 },
michael@0 8147 };
michael@0 8148
michael@0 8149 //@}
michael@0 8150 ///// The justifyRight command /////
michael@0 8151 //@{
michael@0 8152 commands.justifyright = {
michael@0 8153 preservesOverrides: true,
michael@0 8154 // "Justify the selection with alignment "right", then return true."
michael@0 8155 action: function() { justifySelection("right"); return true },
michael@0 8156 indeterm: function() {
michael@0 8157 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8158 // the active range. Return true if among visible editable nodes that
michael@0 8159 // are contained in the result and have no children, at least one has
michael@0 8160 // alignment value "right" and at least one does not. Otherwise return
michael@0 8161 // false."
michael@0 8162 if (!getActiveRange()) {
michael@0 8163 return false;
michael@0 8164 }
michael@0 8165 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8166 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8167 });
michael@0 8168 return nodes.some(function(node) { return getAlignmentValue(node) == "right" })
michael@0 8169 && nodes.some(function(node) { return getAlignmentValue(node) != "right" });
michael@0 8170 }, state: function() {
michael@0 8171 // "Return false if the active range is null. Otherwise, block-extend
michael@0 8172 // the active range. Return true if there is at least one visible
michael@0 8173 // editable node that is contained in the result and has no children,
michael@0 8174 // and all such nodes have alignment value "right". Otherwise return
michael@0 8175 // false."
michael@0 8176 if (!getActiveRange()) {
michael@0 8177 return false;
michael@0 8178 }
michael@0 8179 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8180 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8181 });
michael@0 8182 return nodes.length
michael@0 8183 && nodes.every(function(node) { return getAlignmentValue(node) == "right" });
michael@0 8184 }, value: function() {
michael@0 8185 // "Return the empty string if the active range is null. Otherwise,
michael@0 8186 // block-extend the active range, and return the alignment value of the
michael@0 8187 // first visible editable node that is contained in the result and has
michael@0 8188 // no children. If there is no such node, return "left"."
michael@0 8189 if (!getActiveRange()) {
michael@0 8190 return "";
michael@0 8191 }
michael@0 8192 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
michael@0 8193 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
michael@0 8194 });
michael@0 8195 if (nodes.length) {
michael@0 8196 return getAlignmentValue(nodes[0]);
michael@0 8197 } else {
michael@0 8198 return "left";
michael@0 8199 }
michael@0 8200 },
michael@0 8201 };
michael@0 8202
michael@0 8203 //@}
michael@0 8204 ///// The outdent command /////
michael@0 8205 //@{
michael@0 8206 commands.outdent = {
michael@0 8207 preservesOverrides: true,
michael@0 8208 action: function() {
michael@0 8209 // "Let items be a list of all lis that are ancestor containers of the
michael@0 8210 // range's start and/or end node."
michael@0 8211 //
michael@0 8212 // It's annoying to get this in tree order using functional stuff
michael@0 8213 // without doing getDescendants(document), which is slow, so I do it
michael@0 8214 // imperatively.
michael@0 8215 var items = [];
michael@0 8216 (function(){
michael@0 8217 for (
michael@0 8218 var ancestorContainer = getActiveRange().endContainer;
michael@0 8219 ancestorContainer != getActiveRange().commonAncestorContainer;
michael@0 8220 ancestorContainer = ancestorContainer.parentNode
michael@0 8221 ) {
michael@0 8222 if (isHtmlElement(ancestorContainer, "li")) {
michael@0 8223 items.unshift(ancestorContainer);
michael@0 8224 }
michael@0 8225 }
michael@0 8226 for (
michael@0 8227 var ancestorContainer = getActiveRange().startContainer;
michael@0 8228 ancestorContainer;
michael@0 8229 ancestorContainer = ancestorContainer.parentNode
michael@0 8230 ) {
michael@0 8231 if (isHtmlElement(ancestorContainer, "li")) {
michael@0 8232 items.unshift(ancestorContainer);
michael@0 8233 }
michael@0 8234 }
michael@0 8235 })();
michael@0 8236
michael@0 8237 // "For each item in items, normalize sublists of item."
michael@0 8238 items.forEach(normalizeSublists);
michael@0 8239
michael@0 8240 // "Block-extend the active range, and let new range be the result."
michael@0 8241 var newRange = blockExtend(getActiveRange());
michael@0 8242
michael@0 8243 // "Let node list be a list of nodes, initially empty."
michael@0 8244 //
michael@0 8245 // "For each node node contained in new range, append node to node list
michael@0 8246 // if the last member of node list (if any) is not an ancestor of node;
michael@0 8247 // node is editable; and either node has no editable descendants, or is
michael@0 8248 // an ol or ul, or is an li whose parent is an ol or ul."
michael@0 8249 var nodeList = getContainedNodes(newRange, function(node) {
michael@0 8250 return isEditable(node)
michael@0 8251 && (!getDescendants(node).some(isEditable)
michael@0 8252 || isHtmlElement(node, ["ol", "ul"])
michael@0 8253 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
michael@0 8254 });
michael@0 8255
michael@0 8256 // "While node list is not empty:"
michael@0 8257 while (nodeList.length) {
michael@0 8258 // "While the first member of node list is an ol or ul or is not
michael@0 8259 // the child of an ol or ul, outdent it and remove it from node
michael@0 8260 // list."
michael@0 8261 while (nodeList.length
michael@0 8262 && (isHtmlElement(nodeList[0], ["OL", "UL"])
michael@0 8263 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
michael@0 8264 outdentNode(nodeList.shift());
michael@0 8265 }
michael@0 8266
michael@0 8267 // "If node list is empty, break from these substeps."
michael@0 8268 if (!nodeList.length) {
michael@0 8269 break;
michael@0 8270 }
michael@0 8271
michael@0 8272 // "Let sublist be a list of nodes, initially empty."
michael@0 8273 var sublist = [];
michael@0 8274
michael@0 8275 // "Remove the first member of node list and append it to sublist."
michael@0 8276 sublist.push(nodeList.shift());
michael@0 8277
michael@0 8278 // "While the first member of node list is the nextSibling of the
michael@0 8279 // last member of sublist, and the first member of node list is not
michael@0 8280 // an ol or ul, remove the first member of node list and append it
michael@0 8281 // to sublist."
michael@0 8282 while (nodeList.length
michael@0 8283 && nodeList[0] == sublist[sublist.length - 1].nextSibling
michael@0 8284 && !isHtmlElement(nodeList[0], ["OL", "UL"])) {
michael@0 8285 sublist.push(nodeList.shift());
michael@0 8286 }
michael@0 8287
michael@0 8288 // "Record the values of sublist, and let values be the result."
michael@0 8289 var values = recordValues(sublist);
michael@0 8290
michael@0 8291 // "Split the parent of sublist, with new parent null."
michael@0 8292 splitParent(sublist);
michael@0 8293
michael@0 8294 // "Fix disallowed ancestors of each member of sublist."
michael@0 8295 sublist.forEach(fixDisallowedAncestors);
michael@0 8296
michael@0 8297 // "Restore the values from values."
michael@0 8298 restoreValues(values);
michael@0 8299 }
michael@0 8300
michael@0 8301 // "Return true."
michael@0 8302 return true;
michael@0 8303 }
michael@0 8304 };
michael@0 8305
michael@0 8306 //@}
michael@0 8307
michael@0 8308 //////////////////////////////////
michael@0 8309 ///// Miscellaneous commands /////
michael@0 8310 //////////////////////////////////
michael@0 8311
michael@0 8312 ///// The defaultParagraphSeparator command /////
michael@0 8313 //@{
michael@0 8314 commands.defaultparagraphseparator = {
michael@0 8315 action: function(value) {
michael@0 8316 // "Let value be converted to ASCII lowercase. If value is then equal
michael@0 8317 // to "p" or "div", set the context object's default single-line
michael@0 8318 // container name to value and return true. Otherwise, return false."
michael@0 8319 value = value.toLowerCase();
michael@0 8320 if (value == "p" || value == "div") {
michael@0 8321 defaultSingleLineContainerName = value;
michael@0 8322 return true;
michael@0 8323 }
michael@0 8324 return false;
michael@0 8325 }, value: function() {
michael@0 8326 // "Return the context object's default single-line container name."
michael@0 8327 return defaultSingleLineContainerName;
michael@0 8328 },
michael@0 8329 };
michael@0 8330
michael@0 8331 //@}
michael@0 8332 ///// The selectAll command /////
michael@0 8333 //@{
michael@0 8334 commands.selectall = {
michael@0 8335 // Note, this ignores the whole globalRange/getActiveRange() thing and
michael@0 8336 // works with actual selections. Not suitable for autoimplementation.html.
michael@0 8337 action: function() {
michael@0 8338 // "Let target be the body element of the context object."
michael@0 8339 var target = document.body;
michael@0 8340
michael@0 8341 // "If target is null, let target be the context object's
michael@0 8342 // documentElement."
michael@0 8343 if (!target) {
michael@0 8344 target = document.documentElement;
michael@0 8345 }
michael@0 8346
michael@0 8347 // "If target is null, call getSelection() on the context object, and
michael@0 8348 // call removeAllRanges() on the result."
michael@0 8349 if (!target) {
michael@0 8350 getSelection().removeAllRanges();
michael@0 8351
michael@0 8352 // "Otherwise, call getSelection() on the context object, and call
michael@0 8353 // selectAllChildren(target) on the result."
michael@0 8354 } else {
michael@0 8355 getSelection().selectAllChildren(target);
michael@0 8356 }
michael@0 8357
michael@0 8358 // "Return true."
michael@0 8359 return true;
michael@0 8360 }
michael@0 8361 };
michael@0 8362
michael@0 8363 //@}
michael@0 8364 ///// The styleWithCSS command /////
michael@0 8365 //@{
michael@0 8366 commands.stylewithcss = {
michael@0 8367 action: function(value) {
michael@0 8368 // "If value is an ASCII case-insensitive match for the string
michael@0 8369 // "false", set the CSS styling flag to false. Otherwise, set the
michael@0 8370 // CSS styling flag to true. Either way, return true."
michael@0 8371 cssStylingFlag = String(value).toLowerCase() != "false";
michael@0 8372 return true;
michael@0 8373 }, state: function() { return cssStylingFlag }
michael@0 8374 };
michael@0 8375
michael@0 8376 //@}
michael@0 8377 ///// The useCSS command /////
michael@0 8378 //@{
michael@0 8379 commands.usecss = {
michael@0 8380 action: function(value) {
michael@0 8381 // "If value is an ASCII case-insensitive match for the string "false",
michael@0 8382 // set the CSS styling flag to true. Otherwise, set the CSS styling
michael@0 8383 // flag to false. Either way, return true."
michael@0 8384 cssStylingFlag = String(value).toLowerCase() == "false";
michael@0 8385 return true;
michael@0 8386 }
michael@0 8387 };
michael@0 8388 //@}
michael@0 8389
michael@0 8390 // Some final setup
michael@0 8391 //@{
michael@0 8392 (function() {
michael@0 8393 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
michael@0 8394 // temporary, which means I need an extra closure to not leak the temporaries
michael@0 8395 // into the global namespace. >:(
michael@0 8396 var commandNames = [];
michael@0 8397 for (var command in commands) {
michael@0 8398 commandNames.push(command);
michael@0 8399 }
michael@0 8400 commandNames.forEach(function(command) {
michael@0 8401 // "If a command does not have a relevant CSS property specified, it
michael@0 8402 // defaults to null."
michael@0 8403 if (!("relevantCssProperty" in commands[command])) {
michael@0 8404 commands[command].relevantCssProperty = null;
michael@0 8405 }
michael@0 8406
michael@0 8407 // "If a command has inline command activated values defined but nothing
michael@0 8408 // else defines when it is indeterminate, it is indeterminate if among
michael@0 8409 // formattable nodes effectively contained in the active range, there is at
michael@0 8410 // least one whose effective command value is one of the given values and
michael@0 8411 // at least one whose effective command value is not one of the given
michael@0 8412 // values."
michael@0 8413 if ("inlineCommandActivatedValues" in commands[command]
michael@0 8414 && !("indeterm" in commands[command])) {
michael@0 8415 commands[command].indeterm = function() {
michael@0 8416 if (!getActiveRange()) {
michael@0 8417 return false;
michael@0 8418 }
michael@0 8419
michael@0 8420 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
michael@0 8421 .map(function(node) { return getEffectiveCommandValue(node, command) });
michael@0 8422
michael@0 8423 var matchingValues = values.filter(function(value) {
michael@0 8424 return commands[command].inlineCommandActivatedValues.indexOf(value) != -1;
michael@0 8425 });
michael@0 8426
michael@0 8427 return matchingValues.length >= 1
michael@0 8428 && values.length - matchingValues.length >= 1;
michael@0 8429 };
michael@0 8430 }
michael@0 8431
michael@0 8432 // "If a command has inline command activated values defined, its state is
michael@0 8433 // true if either no formattable node is effectively contained in the
michael@0 8434 // active range, and the active range's start node's effective command
michael@0 8435 // value is one of the given values; or if there is at least one
michael@0 8436 // formattable node effectively contained in the active range, and all of
michael@0 8437 // them have an effective command value equal to one of the given values."
michael@0 8438 if ("inlineCommandActivatedValues" in commands[command]) {
michael@0 8439 commands[command].state = function() {
michael@0 8440 if (!getActiveRange()) {
michael@0 8441 return false;
michael@0 8442 }
michael@0 8443
michael@0 8444 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
michael@0 8445
michael@0 8446 if (nodes.length == 0) {
michael@0 8447 return commands[command].inlineCommandActivatedValues
michael@0 8448 .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1;
michael@0 8449 } else {
michael@0 8450 return nodes.every(function(node) {
michael@0 8451 return commands[command].inlineCommandActivatedValues
michael@0 8452 .indexOf(getEffectiveCommandValue(node, command)) != -1;
michael@0 8453 });
michael@0 8454 }
michael@0 8455 };
michael@0 8456 }
michael@0 8457
michael@0 8458 // "If a command is a standard inline value command, it is indeterminate if
michael@0 8459 // among formattable nodes that are effectively contained in the active
michael@0 8460 // range, there are two that have distinct effective command values. Its
michael@0 8461 // value is the effective command value of the first formattable node that
michael@0 8462 // is effectively contained in the active range; or if there is no such
michael@0 8463 // node, the effective command value of the active range's start node; or
michael@0 8464 // if that is null, the empty string."
michael@0 8465 if ("standardInlineValueCommand" in commands[command]) {
michael@0 8466 commands[command].indeterm = function() {
michael@0 8467 if (!getActiveRange()) {
michael@0 8468 return false;
michael@0 8469 }
michael@0 8470
michael@0 8471 var values = getAllEffectivelyContainedNodes(getActiveRange())
michael@0 8472 .filter(isFormattableNode)
michael@0 8473 .map(function(node) { return getEffectiveCommandValue(node, command) });
michael@0 8474 for (var i = 1; i < values.length; i++) {
michael@0 8475 if (values[i] != values[i - 1]) {
michael@0 8476 return true;
michael@0 8477 }
michael@0 8478 }
michael@0 8479 return false;
michael@0 8480 };
michael@0 8481
michael@0 8482 commands[command].value = function() {
michael@0 8483 if (!getActiveRange()) {
michael@0 8484 return "";
michael@0 8485 }
michael@0 8486
michael@0 8487 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
michael@0 8488
michael@0 8489 if (typeof refNode == "undefined") {
michael@0 8490 refNode = getActiveRange().startContainer;
michael@0 8491 }
michael@0 8492
michael@0 8493 var ret = getEffectiveCommandValue(refNode, command);
michael@0 8494 if (ret === null) {
michael@0 8495 return "";
michael@0 8496 }
michael@0 8497 return ret;
michael@0 8498 };
michael@0 8499 }
michael@0 8500
michael@0 8501 // "If a command preserves overrides, then before taking its action, the
michael@0 8502 // user agent must record current overrides. After taking the action, if
michael@0 8503 // the active range is collapsed, it must restore states and values from
michael@0 8504 // the recorded list."
michael@0 8505 if ("preservesOverrides" in commands[command]) {
michael@0 8506 var oldAction = commands[command].action;
michael@0 8507
michael@0 8508 commands[command].action = function(value) {
michael@0 8509 var overrides = recordCurrentOverrides();
michael@0 8510 var ret = oldAction(value);
michael@0 8511 if (getActiveRange().collapsed) {
michael@0 8512 restoreStatesAndValues(overrides);
michael@0 8513 }
michael@0 8514 return ret;
michael@0 8515 };
michael@0 8516 }
michael@0 8517 });
michael@0 8518 })();
michael@0 8519 //@}
michael@0 8520
michael@0 8521 // vim: foldmarker=@{,@} foldmethod=marker

mercurial