1 /** 2 * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global Node, runtime, core, gui, ops, odf, NodeFilter*/ 26 27 /** 28 * A document that keeps all data related to the mapped document. 29 * @constructor 30 * @implements {ops.Document} 31 * @implements {core.Destroyable} 32 * @param {!odf.OdfCanvas} odfCanvas 33 */ 34 ops.OdtDocument = function OdtDocument(odfCanvas) { 35 "use strict"; 36 37 var self = this, 38 /**@type{!odf.StepUtils}*/ 39 stepUtils, 40 odfUtils = odf.OdfUtils, 41 domUtils = core.DomUtils, 42 /**!Object.<!ops.OdtCursor>*/ 43 cursors = {}, 44 /**!Object.<!ops.Member>*/ 45 members = {}, 46 eventNotifier = new core.EventNotifier([ 47 ops.Document.signalMemberAdded, 48 ops.Document.signalMemberUpdated, 49 ops.Document.signalMemberRemoved, 50 ops.Document.signalCursorAdded, 51 ops.Document.signalCursorRemoved, 52 ops.Document.signalCursorMoved, 53 ops.OdtDocument.signalParagraphChanged, 54 ops.OdtDocument.signalParagraphStyleModified, 55 ops.OdtDocument.signalCommonStyleCreated, 56 ops.OdtDocument.signalCommonStyleDeleted, 57 ops.OdtDocument.signalTableAdded, 58 ops.OdtDocument.signalOperationStart, 59 ops.OdtDocument.signalOperationEnd, 60 ops.OdtDocument.signalProcessingBatchStart, 61 ops.OdtDocument.signalProcessingBatchEnd, 62 ops.OdtDocument.signalUndoStackChanged, 63 ops.OdtDocument.signalStepsInserted, 64 ops.OdtDocument.signalStepsRemoved, 65 ops.OdtDocument.signalMetadataUpdated, 66 ops.OdtDocument.signalAnnotationAdded 67 ]), 68 /**@const*/ 69 FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT, 70 /**@const*/ 71 FILTER_REJECT = core.PositionFilter.FilterResult.FILTER_REJECT, 72 /**@const*/ 73 NEXT = core.StepDirection.NEXT, 74 filter, 75 /**@type{!ops.OdtStepsTranslator}*/ 76 stepsTranslator, 77 lastEditingOp, 78 unsupportedMetadataRemoved = false, 79 /**@const*/ SHOW_ALL = NodeFilter.SHOW_ALL, 80 blacklistedNodes = new gui.BlacklistNamespaceNodeFilter(["urn:webodf:names:cursor", "urn:webodf:names:editinfo"]), 81 odfTextBodyFilter = new gui.OdfTextBodyNodeFilter(), 82 defaultNodeFilter = new core.NodeFilterChain([blacklistedNodes, odfTextBodyFilter]); 83 84 /** 85 * 86 * @param {!Node} rootNode 87 * @return {!core.PositionIterator} 88 */ 89 function createPositionIterator(rootNode) { 90 return new core.PositionIterator(rootNode, SHOW_ALL, defaultNodeFilter, false); 91 } 92 this.createPositionIterator = createPositionIterator; 93 94 /** 95 * Return the office:text element of this document. 96 * @return {!Element} 97 */ 98 function getRootNode() { 99 var element = odfCanvas.odfContainer().getContentElement(), 100 localName = element && element.localName; 101 runtime.assert(localName === "text", "Unsupported content element type '" + localName + "' for OdtDocument"); 102 return element; 103 } 104 /** 105 * Return the office:document element of this document. 106 * @return {!Element} 107 */ 108 this.getDocumentElement = function () { 109 return odfCanvas.odfContainer().rootElement; 110 }; 111 112 this.cloneDocumentElement = function () { 113 var rootElement = self.getDocumentElement(), 114 annotationViewManager = odfCanvas.getAnnotationViewManager(), 115 initialDoc; 116 117 if (annotationViewManager) { 118 annotationViewManager.forgetAnnotations(); 119 } 120 initialDoc = rootElement.cloneNode(true); 121 odfCanvas.refreshAnnotations(); 122 // workaround AnnotationViewManager not fixing up cursor positions after creating the highlighting 123 self.fixCursorPositions(); 124 return initialDoc; 125 }; 126 127 /** 128 * @param {!Element} documentElement 129 */ 130 this.setDocumentElement = function (documentElement) { 131 var odfContainer = odfCanvas.odfContainer(), 132 rootNode; 133 134 eventNotifier.unsubscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted); 135 eventNotifier.unsubscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved); 136 137 // TODO Replace with a neater hack for reloading the Odt tree 138 // Once this is fixed, SelectionView.addOverlays can be removed 139 odfContainer.setRootElement(documentElement); 140 odfCanvas.setOdfContainer(odfContainer, true); 141 odfCanvas.refreshCSS(); 142 rootNode = getRootNode(); 143 stepsTranslator = new ops.OdtStepsTranslator(rootNode, createPositionIterator(rootNode), filter, 500); 144 eventNotifier.subscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted); 145 eventNotifier.subscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved); 146 }; 147 148 /** 149 * @return {!Document} 150 */ 151 function getDOMDocument() { 152 return /**@type{!Document}*/(self.getDocumentElement().ownerDocument); 153 } 154 this.getDOMDocument = getDOMDocument; 155 156 /** 157 * @param {!Node} node 158 * @return {!boolean} 159 */ 160 function isRoot(node) { 161 if ((node.namespaceURI === odf.Namespaces.officens 162 && node.localName === 'text' 163 ) || (node.namespaceURI === odf.Namespaces.officens 164 && node.localName === 'annotation')) { 165 return true; 166 } 167 return false; 168 } 169 170 /** 171 * @param {!Node} node 172 * @return {!Node} 173 */ 174 function getRoot(node) { 175 while (node && !isRoot(node)) { 176 node = /**@type{!Node}*/(node.parentNode); 177 } 178 return node; 179 } 180 this.getRootElement = getRoot; 181 182 /** 183 * A filter that allows a position if it has the same closest 184 * whitelisted root as the specified 'anchor', which can be the cursor 185 * of the given memberid, or a given node 186 * @constructor 187 * @implements {core.PositionFilter} 188 * @param {!string|!Node} anchor 189 */ 190 function RootFilter(anchor) { 191 192 /** 193 * @param {!core.PositionIterator} iterator 194 * @return {!core.PositionFilter.FilterResult} 195 */ 196 this.acceptPosition = function (iterator) { 197 var node = iterator.container(), 198 anchorNode; 199 200 if (typeof anchor === "string") { 201 anchorNode = cursors[anchor].getNode(); 202 } else { 203 anchorNode = anchor; 204 } 205 206 if (getRoot(node) === getRoot(anchorNode)) { 207 return FILTER_ACCEPT; 208 } 209 return FILTER_REJECT; 210 }; 211 } 212 213 /** 214 * Create a new StepIterator instance set to the defined position 215 * 216 * @param {!Node} container 217 * @param {!number} offset 218 * @param {!Array.<!core.PositionFilter>} filters Filter to apply to the iterator positions. If multiple 219 * iterators are provided, they will be combined in order using a PositionFilterChain. 220 * @param {!Node} subTree Subtree to search for step within. Generally a paragraph or document root. Choosing 221 * a smaller subtree allows iteration to end quickly if there are no walkable steps remaining in a particular 222 * direction. This can vastly improve performance. 223 * 224 * @return {!core.StepIterator} 225 */ 226 function createStepIterator(container, offset, filters, subTree) { 227 var positionIterator = createPositionIterator(subTree), 228 filterOrChain, 229 stepIterator; 230 231 if (filters.length === 1) { 232 filterOrChain = filters[0]; 233 } else { 234 filterOrChain = new core.PositionFilterChain(); 235 filters.forEach(filterOrChain.addFilter); 236 } 237 238 stepIterator = new core.StepIterator(filterOrChain, positionIterator); 239 stepIterator.setPosition(container, offset); 240 return stepIterator; 241 } 242 this.createStepIterator = createStepIterator; 243 244 /** 245 * Returns a PositionIterator instance at the 246 * specified starting position 247 * @param {!number} position 248 * @return {!core.PositionIterator} 249 */ 250 function getIteratorAtPosition(position) { 251 var iterator = createPositionIterator(getRootNode()), 252 point = stepsTranslator.convertStepsToDomPoint(position); 253 254 iterator.setUnfilteredPosition(point.node, point.offset); 255 return iterator; 256 } 257 this.getIteratorAtPosition = getIteratorAtPosition; 258 259 /** 260 * Converts the requested step number from root into the equivalent DOM node & offset 261 * pair. If the step is outside the bounds of the document, a RangeError will be thrown. 262 * @param {!number} step 263 * @return {!{node: !Node, offset: !number}} 264 */ 265 this.convertCursorStepToDomPoint = function (step) { 266 return stepsTranslator.convertStepsToDomPoint(step); 267 }; 268 269 /** 270 * Rounds to the first step within the paragraph 271 * @param {!core.StepDirection} step 272 * @return {!boolean} 273 */ 274 function roundUp(step) { 275 return step === NEXT; 276 } 277 278 /** 279 * Converts a DOM node and offset pair to a cursor step. If a rounding direction is not supplied then 280 * the default is to round down to the previous step. 281 * @param {!Node} node 282 * @param {!number} offset 283 * @param {core.StepDirection=} roundDirection Whether to round down to the previous step or round up 284 * to the next step. The default value if unspecified is core.StepDirection.PREVIOUS 285 * @return {!number} 286 */ 287 this.convertDomPointToCursorStep = function (node, offset, roundDirection) { 288 var roundingFunc; 289 if(roundDirection === NEXT) { 290 roundingFunc = roundUp; 291 } 292 293 return stepsTranslator.convertDomPointToSteps(node, offset, roundingFunc); 294 }; 295 296 /** 297 * @param {!{anchorNode: !Node, anchorOffset: !number, focusNode: !Node, focusOffset: !number}} selection 298 * @return {!{position: !number, length: number}} 299 */ 300 this.convertDomToCursorRange = function (selection) { 301 var point1, 302 point2; 303 304 point1 = stepsTranslator.convertDomPointToSteps(selection.anchorNode, selection.anchorOffset); 305 if (selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) { 306 point2 = point1; 307 } else { 308 point2 = stepsTranslator.convertDomPointToSteps(selection.focusNode, selection.focusOffset); 309 } 310 311 return { 312 position: point1, 313 length: point2 - point1 314 }; 315 }; 316 317 /** 318 * Convert a cursor range to a DOM range 319 * @param {!number} position 320 * @param {!number} length 321 * @return {!Range} 322 */ 323 this.convertCursorToDomRange = function (position, length) { 324 var range = getDOMDocument().createRange(), 325 point1, 326 point2; 327 328 point1 = stepsTranslator.convertStepsToDomPoint(position); 329 if (length) { 330 point2 = stepsTranslator.convertStepsToDomPoint(position + length); 331 if (length > 0) { 332 range.setStart(point1.node, point1.offset); 333 range.setEnd(point2.node, point2.offset); 334 } else { 335 range.setStart(point2.node, point2.offset); 336 range.setEnd(point1.node, point1.offset); 337 } 338 } else { 339 range.setStart(point1.node, point1.offset); 340 } 341 return range; 342 }; 343 344 /** 345 * This function will iterate through positions allowed by the position 346 * iterator and count only the text positions. When the amount defined by 347 * offset has been counted, the Text node that that position is returned 348 * as well as the offset in that text node. 349 * Optionally takes a memberid of a cursor, to specifically return the 350 * text node positioned just behind that cursor. 351 * @param {!number} steps 352 * @param {!string=} memberid 353 * @return {?{textNode: !Text, offset: !number}} 354 */ 355 function getTextNodeAtStep(steps, memberid) { 356 var iterator = getIteratorAtPosition(steps), 357 node = iterator.container(), 358 lastTextNode, 359 nodeOffset = 0, 360 cursorNode = null, 361 text; 362 363 if (node.nodeType === Node.TEXT_NODE) { 364 // Iterator has stopped within an existing text node, to put that up as a possible target node 365 lastTextNode = /**@type{!Text}*/(node); 366 nodeOffset = /**@type{!number}*/(iterator.unfilteredDomOffset()); 367 // Always cut in a new empty text node at the requested position. 368 // If this proves to be unnecessary, it will be cleaned up just before the return 369 // after all necessary cursor rearrangements have been performed 370 if (lastTextNode.length > 0) { 371 // The node + offset returned make up the boundary just to the right of the requested step 372 if (nodeOffset > 0) { 373 // In this case, after the split, the right of the requested step is just after the new node 374 lastTextNode = lastTextNode.splitText(nodeOffset); 375 } 376 lastTextNode.parentNode.insertBefore(getDOMDocument().createTextNode(""), lastTextNode); 377 lastTextNode = /**@type{!Text}*/(lastTextNode.previousSibling); 378 nodeOffset = 0; 379 } 380 } else { 381 // There is no text node at the current position, so insert a new one at the current position 382 lastTextNode = getDOMDocument().createTextNode(""); 383 nodeOffset = 0; 384 node.insertBefore(lastTextNode, iterator.rightNode()); 385 } 386 387 if (memberid) { 388 // DEPRECATED: This branch is no longer the recommended way of handling cursor movements DO NOT USE 389 // If the member cursor is as the requested position 390 if (cursors[memberid] && self.getCursorPosition(memberid) === steps) { 391 cursorNode = cursors[memberid].getNode(); 392 // Then move the member's cursor after all adjacent cursors 393 while (cursorNode.nextSibling && cursorNode.nextSibling.localName === "cursor") { 394 // TODO this re-arrange logic will break if there are non-cursor elements in the way 395 // E.g., cursors occupy the same "step", but are on different sides of a span boundary 396 // This is currently avoided by calling fixCursorPositions after (almost) every op 397 // to re-arrange cursors together again 398 cursorNode.parentNode.insertBefore(cursorNode.nextSibling, cursorNode); 399 } 400 if (lastTextNode.length > 0 && lastTextNode.nextSibling !== cursorNode) { 401 // The last text node contains content but is not adjacent to the cursor 402 // This can't be moved, as moving it would move the text content around as well. Yikes! 403 // So, create a new text node to insert data into 404 lastTextNode = getDOMDocument().createTextNode(''); 405 nodeOffset = 0; 406 } 407 // Keep the destination text node right next to the member's cursor, so inserted text pushes the cursor over 408 cursorNode.parentNode.insertBefore(lastTextNode, cursorNode); 409 } 410 } else { 411 // Move all cursors BEFORE the new text node. Any cursors occupying the requested position should not 412 // move when new text is added in the position 413 while (lastTextNode.nextSibling && lastTextNode.nextSibling.localName === "cursor") { 414 // TODO this re-arrange logic will break if there are non-cursor elements in the way 415 // E.g., cursors occupy the same "step", but are on different sides of a span boundary 416 // This is currently avoided by calling fixCursorPositions after (almost) every op 417 // to re-arrange cursors together again 418 lastTextNode.parentNode.insertBefore(lastTextNode.nextSibling, lastTextNode); 419 } 420 } 421 422 // After the above cursor adjustments, if the lastTextNode 423 // has a text node previousSibling, merge them and make the result the lastTextNode 424 while (lastTextNode.previousSibling && lastTextNode.previousSibling.nodeType === Node.TEXT_NODE) { 425 text = /**@type{!Text}*/(lastTextNode.previousSibling); 426 text.appendData(lastTextNode.data); 427 nodeOffset = text.length; 428 lastTextNode = text; 429 lastTextNode.parentNode.removeChild(lastTextNode.nextSibling); 430 } 431 432 // Empty text nodes can be left on either side of the split operations that have occurred 433 while (lastTextNode.nextSibling && lastTextNode.nextSibling.nodeType === Node.TEXT_NODE) { 434 text = /**@type{!Text}*/(lastTextNode.nextSibling); 435 lastTextNode.appendData(text.data); 436 lastTextNode.parentNode.removeChild(text); 437 } 438 439 return {textNode: lastTextNode, offset: nodeOffset }; 440 } 441 442 /** 443 * Called after an operation is executed, this 444 * function will check if the operation is an 445 * 'edit', and in that case will update the 446 * document's metadata, such as dc:creator, 447 * meta:editing-cycles, and dc:creator. 448 * @param {!ops.Operation} op 449 */ 450 function handleOperationExecuted(op) { 451 var opspec = op.spec(), 452 memberId = opspec.memberid, 453 date = new Date(opspec.timestamp).toISOString(), 454 odfContainer = odfCanvas.odfContainer(), 455 /**@type{!{setProperties: !Object, removedProperties: ?Array.<!string>}}*/ 456 changedMetadata, 457 fullName; 458 459 // If the operation is an edit (that changes the 460 // ODF that will be saved), then update metadata. 461 if (op.isEdit) { 462 fullName = self.getMember(memberId).getProperties().fullName; 463 odfContainer.setMetadata({ 464 "dc:creator": fullName, 465 "dc:date": date 466 }, null); 467 468 changedMetadata = { 469 setProperties: { 470 "dc:creator": fullName, 471 "dc:date": date 472 }, 473 removedProperties: [] 474 }; 475 476 // If no previous op was found in this session, 477 // then increment meta:editing-cycles by 1. 478 if (!lastEditingOp) { 479 changedMetadata.setProperties["meta:editing-cycles"] = odfContainer.incrementEditingCycles(); 480 // Remove certain metadata fields that 481 // should be updated as soon as edits happen, 482 // but cannot be because we don't support those yet. 483 if (!unsupportedMetadataRemoved) { 484 odfContainer.setMetadata(null, [ 485 "meta:editing-duration", 486 "meta:document-statistic" 487 ]); 488 } 489 } 490 491 lastEditingOp = op; 492 self.emit(ops.OdtDocument.signalMetadataUpdated, changedMetadata); 493 } 494 } 495 496 /** 497 * Upgrades literal whitespaces (' ') to <text:s> </text:s>, 498 * when given a textNode containing the whitespace and an offset 499 * indicating the location of the whitespace in it. 500 * @param {!Text} textNode 501 * @param {!number} offset 502 * @return {!Element} 503 */ 504 function upgradeWhitespaceToElement(textNode, offset) { 505 runtime.assert(textNode.data[offset] === ' ', "upgradeWhitespaceToElement: textNode.data[offset] should be a literal space"); 506 507 var space = textNode.ownerDocument.createElementNS(odf.Namespaces.textns, 'text:s'), 508 container = textNode.parentNode, 509 adjacentNode = textNode; 510 511 space.appendChild(textNode.ownerDocument.createTextNode(' ')); 512 513 if (textNode.length === 1) { 514 // The space is the only element in this node. Can simply replace it 515 container.replaceChild(space, textNode); 516 } else { 517 textNode.deleteData(offset, 1); 518 if (offset > 0) { // Don't create an empty text node if the offset is 0... 519 if (offset < textNode.length) { 520 // Don't split if offset === textNode.length as this would add an empty text node after 521 textNode.splitText(offset); 522 } 523 adjacentNode = textNode.nextSibling; 524 } 525 container.insertBefore(space, adjacentNode); 526 } 527 return space; 528 } 529 530 /** 531 * @param {!number} step 532 * @return {undefined} 533 */ 534 function upgradeWhitespacesAtPosition(step) { 535 var positionIterator = getIteratorAtPosition(step), 536 stepIterator = new core.StepIterator(filter, positionIterator), 537 contentBounds, 538 /**@type{?Node}*/ 539 container, 540 offset, 541 stepsToUpgrade = 2; 542 543 // The step passed into this function is the point of change. Need to 544 // upgrade whitespace to the left of the current step, and to the left of the next step 545 runtime.assert(stepIterator.isStep(), "positionIterator is not at a step (requested step: " + step + ")"); 546 547 do { 548 contentBounds = stepUtils.getContentBounds(stepIterator); 549 if (contentBounds) { 550 container = contentBounds.container; 551 offset = contentBounds.startOffset; 552 if (container.nodeType === Node.TEXT_NODE 553 && odfUtils.isSignificantWhitespace(/**@type{!Text}*/(container), offset)) { 554 container = upgradeWhitespaceToElement(/**@type{!Text}*/(container), offset); 555 // Reset the iterator position to the same step it was just on, which was just to 556 // the right of a space 557 stepIterator.setPosition(container, container.childNodes.length); 558 stepIterator.roundToPreviousStep(); 559 } 560 } 561 stepsToUpgrade -= 1; 562 } while (stepsToUpgrade > 0 && stepIterator.nextStep()); 563 } 564 565 /** 566 * Upgrades any significant whitespace at the requested step, and one step right of the given 567 * position to space elements. 568 * @param {!number} step 569 * @return {undefined} 570 */ 571 this.upgradeWhitespacesAtPosition = upgradeWhitespacesAtPosition; 572 573 /** 574 * Returns the maximum available offset for the specified node. 575 * @param {!Node} node 576 * @return {!number} 577 */ 578 function maxOffset(node) { 579 return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length; 580 } 581 582 /** 583 * Downgrades white space elements to normal spaces at the step iterators current step, and one step 584 * to the right. 585 * 586 * @param {!core.StepIterator} stepIterator 587 * @return {undefined} 588 */ 589 function downgradeWhitespaces(stepIterator) { 590 var contentBounds, 591 /**@type{!Node}*/ 592 container, 593 modifiedNodes = [], 594 lastChild, 595 stepsToUpgrade = 2; 596 597 // The step passed into this function is the point of change. Need to 598 // downgrade whitespace to the left of the current step, and to the left of the next step 599 runtime.assert(stepIterator.isStep(), "positionIterator is not at a step"); 600 601 do { 602 contentBounds = stepUtils.getContentBounds(stepIterator); 603 if (contentBounds) { 604 container = contentBounds.container; 605 if (odfUtils.isDowngradableSpaceElement(container)) { 606 lastChild = /**@type{!Node}*/(container.lastChild); 607 while(container.firstChild) { 608 // Merge contained space text node up to replace container 609 modifiedNodes.push(container.firstChild); 610 container.parentNode.insertBefore(container.firstChild, container); 611 } 612 container.parentNode.removeChild(container); 613 // Reset the iterator position to the same step it was just on, which was just to 614 // the right of a space 615 stepIterator.setPosition(lastChild, maxOffset(lastChild)); 616 stepIterator.roundToPreviousStep(); 617 } 618 } 619 stepsToUpgrade -= 1; 620 } while (stepsToUpgrade > 0 && stepIterator.nextStep()); 621 622 modifiedNodes.forEach(domUtils.normalizeTextNodes); 623 } 624 this.downgradeWhitespaces = downgradeWhitespaces; 625 626 /** 627 * Downgrades white space elements to normal spaces at the specified step, and one step 628 * to the right. 629 * 630 * @param {!number} step 631 * @return {undefined} 632 */ 633 this.downgradeWhitespacesAtPosition = function (step) { 634 var positionIterator = getIteratorAtPosition(step), 635 stepIterator = new core.StepIterator(filter, positionIterator); 636 637 downgradeWhitespaces(stepIterator); 638 }; 639 640 /** 641 * This function will return the Text node as well as the offset in that text node 642 * of the cursor. 643 * @param {!number} position 644 * @param {!string=} memberid 645 * @return {?{textNode: !Text, offset: !number}} 646 */ 647 this.getTextNodeAtStep = getTextNodeAtStep; 648 649 /** 650 * Returns the closest parent paragraph or root to the supplied container and offset 651 * @param {!Node} container 652 * @param {!number} offset 653 * @param {!Node} root 654 * 655 * @return {!Node} 656 */ 657 function paragraphOrRoot(container, offset, root) { 658 var node = container.childNodes.item(offset) || container, 659 paragraph = odfUtils.getParagraphElement(node); 660 if (paragraph && domUtils.containsNode(root, paragraph)) { 661 // Only return the paragraph if it is contained within the destination root 662 return /**@type{!Node}*/(paragraph); 663 } 664 // Otherwise the step filter should be contained within the supplied root 665 return root; 666 } 667 668 /** 669 * Iterates through all cursors and checks if they are in 670 * walkable positions; if not, move the cursor 1 filtered step backward 671 * which guarantees walkable state for all cursors, 672 * while keeping them inside the same root. An event will be raised for this cursor if it is moved 673 */ 674 this.fixCursorPositions = function () { 675 Object.keys(cursors).forEach(function (memberId) { 676 var cursor = cursors[memberId], 677 root = getRoot(cursor.getNode()), 678 rootFilter = self.createRootFilter(root), 679 subTree, 680 startPoint, 681 endPoint, 682 selectedRange, 683 cursorMoved = false; 684 685 selectedRange = cursor.getSelectedRange(); 686 subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, root); 687 startPoint = createStepIterator(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, 688 [filter, rootFilter], subTree); 689 690 if (!selectedRange.collapsed) { 691 subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, root); 692 endPoint = createStepIterator(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, 693 [filter, rootFilter], subTree); 694 } else { 695 endPoint = startPoint; 696 } 697 698 if (!startPoint.isStep() || !endPoint.isStep()) { 699 cursorMoved = true; 700 runtime.assert(startPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId); 701 selectedRange.setStart(startPoint.container(), startPoint.offset()); 702 runtime.assert(endPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId); 703 selectedRange.setEnd(endPoint.container(), endPoint.offset()); 704 } else if (startPoint.container() === endPoint.container() && startPoint.offset() === endPoint.offset()) { 705 // The range *should* be collapsed 706 if (!selectedRange.collapsed || cursor.getAnchorNode() !== cursor.getNode()) { 707 // It might not be collapsed if there are other unwalkable nodes (e.g., cursors) 708 // between the cursor and anchor nodes. In this case, force the cursor to collapse 709 cursorMoved = true; 710 selectedRange.setStart(startPoint.container(), startPoint.offset()); 711 selectedRange.collapse(true); 712 } 713 } 714 715 if (cursorMoved) { 716 cursor.setSelectedRange(selectedRange, cursor.hasForwardSelection()); 717 self.emit(ops.Document.signalCursorMoved, cursor); 718 } 719 }); 720 }; 721 722 /** 723 * This function returns the position in ODF world of the cursor of the member. 724 * @param {!string} memberid 725 * @return {!number} 726 */ 727 this.getCursorPosition = function (memberid) { 728 var cursor = cursors[memberid]; 729 return cursor ? stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0) : 0; 730 }; 731 732 /** 733 * This function returns the position and selection length in ODF world of 734 * the cursor of the member. 735 * position is always the number of steps from root node to the anchor node 736 * length is the number of steps from anchor node to focus node 737 * !IMPORTANT! length is a vector, and may be negative if the cursor selection 738 * is reversed (i.e., user clicked and dragged the cursor backwards) 739 * @param {!string} memberid 740 * @return {{position: !number, length: !number}} 741 */ 742 this.getCursorSelection = function (memberid) { 743 var cursor = cursors[memberid], 744 focusPosition = 0, 745 anchorPosition = 0; 746 if (cursor) { 747 focusPosition = stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0); 748 anchorPosition = stepsTranslator.convertDomPointToSteps(cursor.getAnchorNode(), 0); 749 } 750 return { 751 position: anchorPosition, 752 length: focusPosition - anchorPosition 753 }; 754 }; 755 /** 756 * @return {!core.PositionFilter} 757 */ 758 this.getPositionFilter = function () { 759 return filter; 760 }; 761 762 /** 763 * @return {!odf.OdfCanvas} 764 */ 765 this.getOdfCanvas = function () { 766 return odfCanvas; 767 }; 768 769 /** 770 * @return {!ops.Canvas} 771 */ 772 this.getCanvas = function () { 773 return odfCanvas; 774 }; 775 776 /** 777 * @return {!Element} 778 */ 779 this.getRootNode = getRootNode; 780 781 /** 782 * @param {!ops.Member} member 783 * @return {undefined} 784 */ 785 this.addMember = function (member) { 786 runtime.assert(members[member.getMemberId()] === undefined, "This member already exists"); 787 members[member.getMemberId()] = member; 788 }; 789 790 /** 791 * @param {!string} memberId 792 * @return {?ops.Member} 793 */ 794 this.getMember = function (memberId) { 795 return members.hasOwnProperty(memberId) ? members[memberId] : null; 796 }; 797 798 /** 799 * @param {!string} memberId 800 * @return {undefined} 801 */ 802 this.removeMember = function (memberId) { 803 delete members[memberId]; 804 }; 805 806 /** 807 * @param {!string} memberid 808 * @return {ops.OdtCursor} 809 */ 810 this.getCursor = function (memberid) { 811 return cursors[memberid]; 812 }; 813 814 /** 815 * @return {!Array.<string>} 816 */ 817 this.getMemberIds = function () { 818 var list = [], 819 /**@type{string}*/ 820 i; 821 for (i in cursors) { 822 if (cursors.hasOwnProperty(i)) { 823 list.push(cursors[i].getMemberId()); 824 } 825 } 826 return list; 827 }; 828 829 /** 830 * Adds the specified cursor to the ODT document. The cursor will be collapsed 831 * to the first available cursor position in the document. 832 * @param {!ops.OdtCursor} cursor 833 * @return {undefined} 834 */ 835 this.addCursor = function (cursor) { 836 runtime.assert(Boolean(cursor), "OdtDocument::addCursor without cursor"); 837 var memberid = cursor.getMemberId(), 838 initialSelection = self.convertCursorToDomRange(0, 0); 839 840 runtime.assert(typeof memberid === "string", "OdtDocument::addCursor has cursor without memberid"); 841 runtime.assert(!cursors[memberid], "OdtDocument::addCursor is adding a duplicate cursor with memberid " + memberid); 842 cursor.setSelectedRange(initialSelection, true); 843 844 cursors[memberid] = cursor; 845 }; 846 847 /** 848 * @param {!string} memberid 849 * @return {!boolean} 850 */ 851 this.removeCursor = function (memberid) { 852 var cursor = cursors[memberid]; 853 if (cursor) { 854 cursor.removeFromDocument(); 855 delete cursors[memberid]; 856 self.emit(ops.Document.signalCursorRemoved, memberid); 857 return true; 858 } 859 return false; 860 }; 861 862 /** 863 * Moves the cursor/selection of a given memberid to the 864 * given position+length combination and adopts the given 865 * selectionType. 866 * It is the caller's responsibility to decide if and when 867 * to subsequently fire signalCursorMoved. 868 * @param {!string} memberid 869 * @param {!number} position 870 * @param {!number} length 871 * @param {!string=} selectionType 872 * @return {undefined} 873 */ 874 this.moveCursor = function (memberid, position, length, selectionType) { 875 var cursor = cursors[memberid], 876 selectionRange = self.convertCursorToDomRange(position, length); 877 if (cursor) { 878 cursor.setSelectedRange(selectionRange, length >= 0); 879 cursor.setSelectionType(selectionType || ops.OdtCursor.RangeSelection); 880 } 881 }; 882 883 /** 884 * @return {!odf.Formatting} 885 */ 886 this.getFormatting = function () { 887 return odfCanvas.getFormatting(); 888 }; 889 890 /** 891 * @param {!string} eventid 892 * @param {*} args 893 * @return {undefined} 894 */ 895 this.emit = function (eventid, args) { 896 eventNotifier.emit(eventid, args); 897 }; 898 899 /** 900 * @param {!string} eventid 901 * @param {!Function} cb 902 * @return {undefined} 903 */ 904 this.subscribe = function (eventid, cb) { 905 eventNotifier.subscribe(eventid, cb); 906 }; 907 908 /** 909 * @param {!string} eventid 910 * @param {!Function} cb 911 * @return {undefined} 912 */ 913 this.unsubscribe = function (eventid, cb) { 914 eventNotifier.unsubscribe(eventid, cb); 915 }; 916 917 /** 918 * @param {string|!Node} inputMemberId 919 * @return {!RootFilter} 920 */ 921 this.createRootFilter = function (inputMemberId) { 922 return new RootFilter(inputMemberId); 923 }; 924 925 /** 926 * @param {!function(!Object=)} callback, passing an error object in case of error 927 * @return {undefined} 928 */ 929 this.close = function (callback) { 930 // TODO: check if anything needs to be cleaned up 931 callback(); 932 }; 933 934 /** 935 * @param {!function(!Error=)} callback, passing an error object in case of error 936 * @return {undefined} 937 */ 938 this.destroy = function (callback) { 939 callback(); 940 }; 941 942 /** 943 * @return {undefined} 944 */ 945 function init() { 946 var rootNode = getRootNode(); 947 948 filter = new ops.TextPositionFilter(); 949 stepUtils = new odf.StepUtils(); 950 stepsTranslator = new ops.OdtStepsTranslator(rootNode, createPositionIterator(rootNode), filter, 500); 951 eventNotifier.subscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted); 952 eventNotifier.subscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved); 953 eventNotifier.subscribe(ops.OdtDocument.signalOperationEnd, handleOperationExecuted); 954 eventNotifier.subscribe(ops.OdtDocument.signalProcessingBatchEnd, core.Task.processTasks); 955 } 956 init(); 957 }; 958 959 /**@const*/ops.OdtDocument.signalParagraphChanged = "paragraph/changed"; 960 /**@const*/ops.OdtDocument.signalTableAdded = "table/added"; 961 /**@const*/ops.OdtDocument.signalCommonStyleCreated = "style/created"; 962 /**@const*/ops.OdtDocument.signalCommonStyleDeleted = "style/deleted"; 963 /**@const*/ops.OdtDocument.signalParagraphStyleModified = "paragraphstyle/modified"; 964 /**@const*/ops.OdtDocument.signalOperationStart = "operation/start"; 965 /**@const*/ops.OdtDocument.signalOperationEnd = "operation/end"; 966 /**@const*/ops.OdtDocument.signalProcessingBatchStart = "router/batchstart"; 967 /**@const*/ops.OdtDocument.signalProcessingBatchEnd = "router/batchend"; 968 /**@const*/ops.OdtDocument.signalUndoStackChanged = "undo/changed"; 969 /**@const*/ops.OdtDocument.signalStepsInserted = "steps/inserted"; 970 /**@const*/ops.OdtDocument.signalStepsRemoved = "steps/removed"; 971 /**@const*/ops.OdtDocument.signalMetadataUpdated = "metadata/updated"; 972 /**@const*/ops.OdtDocument.signalAnnotationAdded = "annotation/added"; 973 974 // vim:expandtab 975