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