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 * @return {!Document} 113 */ 114 this.getDOMDocument = function () { 115 return /**@type{!Document}*/(this.getDocumentElement().ownerDocument); 116 }; 117 118 this.cloneDocumentElement = function () { 119 var rootElement = self.getDocumentElement(), 120 annotationViewManager = odfCanvas.getAnnotationViewManager(), 121 initialDoc; 122 123 if (annotationViewManager) { 124 annotationViewManager.forgetAnnotations(); 125 } 126 initialDoc = rootElement.cloneNode(true); 127 odfCanvas.refreshAnnotations(); 128 return initialDoc; 129 }; 130 131 /** 132 * @param {!Element} documentElement 133 */ 134 this.setDocumentElement = function (documentElement) { 135 var odfContainer = odfCanvas.odfContainer(); 136 // TODO Replace with a neater hack for reloading the Odt tree 137 // Once this is fixed, SelectionView.addOverlays & OdtStepsTranslator.verifyRootNode can be largely removed 138 odfContainer.setRootElement(documentElement); 139 odfCanvas.setOdfContainer(odfContainer, true); 140 odfCanvas.refreshCSS(); 141 }; 142 143 /** 144 * @return {!Document} 145 */ 146 function getDOMDocument() { 147 return /**@type{!Document}*/(self.getDocumentElement().ownerDocument); 148 } 149 this.getDOMDocument = getDOMDocument; 150 151 /** 152 * @param {!Node} node 153 * @return {!boolean} 154 */ 155 function isRoot(node) { 156 if ((node.namespaceURI === odf.Namespaces.officens 157 && node.localName === 'text' 158 ) || (node.namespaceURI === odf.Namespaces.officens 159 && node.localName === 'annotation')) { 160 return true; 161 } 162 return false; 163 } 164 165 /** 166 * @param {!Node} node 167 * @return {!Node} 168 */ 169 function getRoot(node) { 170 while (node && !isRoot(node)) { 171 node = /**@type{!Node}*/(node.parentNode); 172 } 173 return node; 174 } 175 this.getRootElement = getRoot; 176 177 /** 178 * A filter that allows a position if it has the same closest 179 * whitelisted root as the specified 'anchor', which can be the cursor 180 * of the given memberid, or a given node 181 * @constructor 182 * @implements {core.PositionFilter} 183 * @param {!string|!Node} anchor 184 */ 185 function RootFilter(anchor) { 186 187 /** 188 * @param {!core.PositionIterator} iterator 189 * @return {!core.PositionFilter.FilterResult} 190 */ 191 this.acceptPosition = function (iterator) { 192 var node = iterator.container(), 193 anchorNode; 194 195 if (typeof anchor === "string") { 196 anchorNode = cursors[anchor].getNode(); 197 } else { 198 anchorNode = anchor; 199 } 200 201 if (getRoot(node) === getRoot(anchorNode)) { 202 return FILTER_ACCEPT; 203 } 204 return FILTER_REJECT; 205 }; 206 } 207 208 /** 209 * Create a new StepIterator instance set to the defined position 210 * 211 * @param {!Node} container 212 * @param {!number} offset 213 * @param {!Array.<!core.PositionFilter>} filters Filter to apply to the iterator positions. If multiple 214 * iterators are provided, they will be combined in order using a PositionFilterChain. 215 * @param {!Node} subTree Subtree to search for step within. Generally a paragraph or document root. Choosing 216 * a smaller subtree allows iteration to end quickly if there are no walkable steps remaining in a particular 217 * direction. This can vastly improve performance. 218 * 219 * @return {!core.StepIterator} 220 */ 221 function createStepIterator(container, offset, filters, subTree) { 222 var positionIterator = createPositionIterator(subTree), 223 filterOrChain, 224 stepIterator; 225 226 if (filters.length === 1) { 227 filterOrChain = filters[0]; 228 } else { 229 filterOrChain = new core.PositionFilterChain(); 230 filters.forEach(filterOrChain.addFilter); 231 } 232 233 stepIterator = new core.StepIterator(filterOrChain, positionIterator); 234 stepIterator.setPosition(container, offset); 235 return stepIterator; 236 } 237 this.createStepIterator = createStepIterator; 238 239 /** 240 * Returns a PositionIterator instance at the 241 * specified starting position 242 * @param {!number} position 243 * @return {!core.PositionIterator} 244 */ 245 function getIteratorAtPosition(position) { 246 var iterator = createPositionIterator(getRootNode()), 247 point = stepsTranslator.convertStepsToDomPoint(position); 248 249 iterator.setUnfilteredPosition(point.node, point.offset); 250 return iterator; 251 } 252 this.getIteratorAtPosition = getIteratorAtPosition; 253 254 /** 255 * Converts the requested step number from root into the equivalent DOM node & offset 256 * pair. If the step is outside the bounds of the document, a RangeError will be thrown. 257 * @param {!number} step 258 * @return {!{node: !Node, offset: !number}} 259 */ 260 this.convertCursorStepToDomPoint = function (step) { 261 return stepsTranslator.convertStepsToDomPoint(step); 262 }; 263 264 /** 265 * Rounds to the first step within the paragraph 266 * @param {!core.StepDirection} step 267 * @return {!boolean} 268 */ 269 function roundUp(step) { 270 return step === NEXT; 271 } 272 273 /** 274 * Converts a DOM node and offset pair to a cursor step. If a rounding direction is not supplied then 275 * the default is to round down to the previous step. 276 * @param {!Node} node 277 * @param {!number} offset 278 * @param {core.StepDirection=} roundDirection Whether to round down to the previous step or round up 279 * to the next step. The default value if unspecified is core.StepDirection.PREVIOUS 280 * @return {!number} 281 */ 282 this.convertDomPointToCursorStep = function (node, offset, roundDirection) { 283 var roundingFunc; 284 if(roundDirection === NEXT) { 285 roundingFunc = roundUp; 286 } 287 288 return stepsTranslator.convertDomPointToSteps(node, offset, roundingFunc); 289 }; 290 291 /** 292 * @param {!{anchorNode: !Node, anchorOffset: !number, focusNode: !Node, focusOffset: !number}} selection 293 * @return {!{position: !number, length: number}} 294 */ 295 this.convertDomToCursorRange = function (selection) { 296 var point1, 297 point2; 298 299 point1 = stepsTranslator.convertDomPointToSteps(selection.anchorNode, selection.anchorOffset); 300 if (selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) { 301 point2 = point1; 302 } else { 303 point2 = stepsTranslator.convertDomPointToSteps(selection.focusNode, selection.focusOffset); 304 } 305 306 return { 307 position: point1, 308 length: point2 - point1 309 }; 310 }; 311 312 /** 313 * Convert a cursor range to a DOM range 314 * @param {!number} position 315 * @param {!number} length 316 * @return {!Range} 317 */ 318 this.convertCursorToDomRange = function (position, length) { 319 var range = getDOMDocument().createRange(), 320 point1, 321 point2; 322 323 point1 = stepsTranslator.convertStepsToDomPoint(position); 324 if (length) { 325 point2 = stepsTranslator.convertStepsToDomPoint(position + length); 326 if (length > 0) { 327 range.setStart(point1.node, point1.offset); 328 range.setEnd(point2.node, point2.offset); 329 } else { 330 range.setStart(point2.node, point2.offset); 331 range.setEnd(point1.node, point1.offset); 332 } 333 } else { 334 range.setStart(point1.node, point1.offset); 335 } 336 return range; 337 }; 338 339 /** 340 * This function will iterate through positions allowed by the position 341 * iterator and count only the text positions. When the amount defined by 342 * offset has been counted, the Text node that that position is returned 343 * as well as the offset in that text node. 344 * Optionally takes a memberid of a cursor, to specifically return the 345 * text node positioned just behind that cursor. 346 * @param {!number} steps 347 * @param {!string=} memberid 348 * @return {?{textNode: !Text, offset: !number}} 349 */ 350 function getTextNodeAtStep(steps, memberid) { 351 var iterator = getIteratorAtPosition(steps), 352 node = iterator.container(), 353 lastTextNode, 354 nodeOffset = 0, 355 cursorNode = null, 356 text; 357 358 if (node.nodeType === Node.TEXT_NODE) { 359 // Iterator has stopped within an existing text node, to put that up as a possible target node 360 lastTextNode = /**@type{!Text}*/(node); 361 nodeOffset = /**@type{!number}*/(iterator.unfilteredDomOffset()); 362 // Always cut in a new empty text node at the requested position. 363 // If this proves to be unnecessary, it will be cleaned up just before the return 364 // after all necessary cursor rearrangements have been performed 365 if (lastTextNode.length > 0) { 366 // The node + offset returned make up the boundary just to the right of the requested step 367 if (nodeOffset > 0) { 368 // In this case, after the split, the right of the requested step is just after the new node 369 lastTextNode = lastTextNode.splitText(nodeOffset); 370 } 371 lastTextNode.parentNode.insertBefore(getDOMDocument().createTextNode(""), lastTextNode); 372 lastTextNode = /**@type{!Text}*/(lastTextNode.previousSibling); 373 nodeOffset = 0; 374 } 375 } else { 376 // There is no text node at the current position, so insert a new one at the current position 377 lastTextNode = getDOMDocument().createTextNode(""); 378 nodeOffset = 0; 379 node.insertBefore(lastTextNode, iterator.rightNode()); 380 } 381 382 if (memberid) { 383 // DEPRECATED: This branch is no longer the recommended way of handling cursor movements DO NOT USE 384 // If the member cursor is as the requested position 385 if (cursors[memberid] && self.getCursorPosition(memberid) === steps) { 386 cursorNode = cursors[memberid].getNode(); 387 // Then move the member's cursor after all adjacent cursors 388 while (cursorNode.nextSibling && cursorNode.nextSibling.localName === "cursor") { 389 // TODO this re-arrange logic will break if there are non-cursor elements in the way 390 // E.g., cursors occupy the same "step", but are on different sides of a span boundary 391 // This is currently avoided by calling fixCursorPositions after (almost) every op 392 // to re-arrange cursors together again 393 cursorNode.parentNode.insertBefore(cursorNode.nextSibling, cursorNode); 394 } 395 if (lastTextNode.length > 0 && lastTextNode.nextSibling !== cursorNode) { 396 // The last text node contains content but is not adjacent to the cursor 397 // This can't be moved, as moving it would move the text content around as well. Yikes! 398 // So, create a new text node to insert data into 399 lastTextNode = getDOMDocument().createTextNode(''); 400 nodeOffset = 0; 401 } 402 // Keep the destination text node right next to the member's cursor, so inserted text pushes the cursor over 403 cursorNode.parentNode.insertBefore(lastTextNode, cursorNode); 404 } 405 } else { 406 // Move all cursors BEFORE the new text node. Any cursors occupying the requested position should not 407 // move when new text is added in the position 408 while (lastTextNode.nextSibling && lastTextNode.nextSibling.localName === "cursor") { 409 // TODO this re-arrange logic will break if there are non-cursor elements in the way 410 // E.g., cursors occupy the same "step", but are on different sides of a span boundary 411 // This is currently avoided by calling fixCursorPositions after (almost) every op 412 // to re-arrange cursors together again 413 lastTextNode.parentNode.insertBefore(lastTextNode.nextSibling, lastTextNode); 414 } 415 } 416 417 // After the above cursor adjustments, if the lastTextNode 418 // has a text node previousSibling, merge them and make the result the lastTextNode 419 while (lastTextNode.previousSibling && lastTextNode.previousSibling.nodeType === Node.TEXT_NODE) { 420 text = /**@type{!Text}*/(lastTextNode.previousSibling); 421 text.appendData(lastTextNode.data); 422 nodeOffset = text.length; 423 lastTextNode = text; 424 lastTextNode.parentNode.removeChild(lastTextNode.nextSibling); 425 } 426 427 // Empty text nodes can be left on either side of the split operations that have occurred 428 while (lastTextNode.nextSibling && lastTextNode.nextSibling.nodeType === Node.TEXT_NODE) { 429 text = /**@type{!Text}*/(lastTextNode.nextSibling); 430 lastTextNode.appendData(text.data); 431 lastTextNode.parentNode.removeChild(text); 432 } 433 434 return {textNode: lastTextNode, offset: nodeOffset }; 435 } 436 437 /** 438 * Called after an operation is executed, this 439 * function will check if the operation is an 440 * 'edit', and in that case will update the 441 * document's metadata, such as dc:creator, 442 * meta:editing-cycles, and dc:creator. 443 * @param {!ops.Operation} op 444 */ 445 function handleOperationExecuted(op) { 446 var opspec = op.spec(), 447 memberId = opspec.memberid, 448 date = new Date(opspec.timestamp).toISOString(), 449 odfContainer = odfCanvas.odfContainer(), 450 /**@type{!{setProperties: !Object, removedProperties: ?Array.<!string>}}*/ 451 changedMetadata = { 452 setProperties: {}, 453 removedProperties: [] 454 }, 455 fullName; 456 457 // If the operation is an edit (that changes the 458 // ODF that will be saved), then update metadata. 459 if (op.isEdit) { 460 if (opspec.optype === "UpdateMetadata") { 461 // HACK: Cannot typecast this to OpUpdateMetadata's spec because that would be a cyclic dependency, 462 // therefore forcibly typecast this to advertise the two required properties. Also, deep clone to avoid 463 // unintended modification of the op spec. 464 changedMetadata.setProperties = /**@type{!Object}*/(JSON.parse(JSON.stringify(/**@type{!{setProperties: !Object}}*/(opspec).setProperties))); 465 if (/**@type{!{removedProperties: ?{attributes: !string}}}*/(opspec).removedProperties) { 466 changedMetadata.removedProperties = /**@type{!{removedProperties: ?{attributes: !string}}}*/(opspec).removedProperties.attributes.split(','); 467 } 468 } 469 470 fullName = self.getMember(memberId).getProperties().fullName; 471 odfContainer.setMetadata({ 472 "dc:creator": fullName, 473 "dc:date": date 474 }, null); 475 changedMetadata.setProperties["dc:creator"] = fullName; 476 changedMetadata.setProperties["dc:date"] = date; 477 478 // If no previous op was found in this session, 479 // then increment meta:editing-cycles by 1. 480 if (!lastEditingOp) { 481 changedMetadata.setProperties["meta:editing-cycles"] = odfContainer.incrementEditingCycles(); 482 // Remove certain metadata fields that 483 // should be updated as soon as edits happen, 484 // but cannot be because we don't support those yet. 485 if (!unsupportedMetadataRemoved) { 486 odfContainer.setMetadata(null, [ 487 "meta:editing-duration", 488 "meta:document-statistic" 489 ]); 490 } 491 } 492 493 lastEditingOp = op; 494 self.emit(ops.OdtDocument.signalMetadataUpdated, changedMetadata); 495 } 496 } 497 498 /** 499 * Upgrades literal whitespaces (' ') to <text:s> </text:s>, 500 * when given a textNode containing the whitespace and an offset 501 * indicating the location of the whitespace in it. 502 * @param {!Text} textNode 503 * @param {!number} offset 504 * @return {!Element} 505 */ 506 function upgradeWhitespaceToElement(textNode, offset) { 507 runtime.assert(textNode.data[offset] === ' ', "upgradeWhitespaceToElement: textNode.data[offset] should be a literal space"); 508 509 var space = textNode.ownerDocument.createElementNS(odf.Namespaces.textns, 'text:s'), 510 container = textNode.parentNode, 511 adjacentNode = textNode; 512 513 space.appendChild(textNode.ownerDocument.createTextNode(' ')); 514 515 if (textNode.length === 1) { 516 // The space is the only element in this node. Can simply replace it 517 container.replaceChild(space, textNode); 518 } else { 519 textNode.deleteData(offset, 1); 520 if (offset > 0) { // Don't create an empty text node if the offset is 0... 521 if (offset < textNode.length) { 522 // Don't split if offset === textNode.length as this would add an empty text node after 523 textNode.splitText(offset); 524 } 525 adjacentNode = textNode.nextSibling; 526 } 527 container.insertBefore(space, adjacentNode); 528 } 529 return space; 530 } 531 532 /** 533 * @param {!number} step 534 * @return {undefined} 535 */ 536 function upgradeWhitespacesAtPosition(step) { 537 var positionIterator = getIteratorAtPosition(step), 538 stepIterator = new core.StepIterator(filter, positionIterator), 539 contentBounds, 540 /**@type{?Node}*/ 541 container, 542 offset, 543 stepsToUpgrade = 2; 544 545 // The step passed into this function is the point of change. Need to 546 // upgrade whitespace to the left of the current step, and to the left of the next step 547 runtime.assert(stepIterator.isStep(), "positionIterator is not at a step (requested step: " + step + ")"); 548 549 do { 550 contentBounds = stepUtils.getContentBounds(stepIterator); 551 if (contentBounds) { 552 container = contentBounds.container; 553 offset = contentBounds.startOffset; 554 if (container.nodeType === Node.TEXT_NODE 555 && odfUtils.isSignificantWhitespace(/**@type{!Text}*/(container), offset)) { 556 container = upgradeWhitespaceToElement(/**@type{!Text}*/(container), offset); 557 // Reset the iterator position to the same step it was just on, which was just to 558 // the right of a space 559 stepIterator.setPosition(container, container.childNodes.length); 560 stepIterator.roundToPreviousStep(); 561 } 562 } 563 stepsToUpgrade -= 1; 564 } while (stepsToUpgrade > 0 && stepIterator.nextStep()); 565 } 566 567 /** 568 * Upgrades any significant whitespace at the requested step, and one step right of the given 569 * position to space elements. 570 * @param {!number} step 571 * @return {undefined} 572 */ 573 this.upgradeWhitespacesAtPosition = upgradeWhitespacesAtPosition; 574 575 /** 576 * Returns the maximum available offset for the specified node. 577 * @param {!Node} node 578 * @return {!number} 579 */ 580 function maxOffset(node) { 581 return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length; 582 } 583 584 /** 585 * Downgrades white space elements to normal spaces at the step iterators current step, and one step 586 * to the right. 587 * 588 * @param {!core.StepIterator} stepIterator 589 * @return {undefined} 590 */ 591 function downgradeWhitespaces(stepIterator) { 592 var contentBounds, 593 /**@type{!Node}*/ 594 container, 595 modifiedNodes = [], 596 lastChild, 597 stepsToUpgrade = 2; 598 599 // The step passed into this function is the point of change. Need to 600 // downgrade whitespace to the left of the current step, and to the left of the next step 601 runtime.assert(stepIterator.isStep(), "positionIterator is not at a step"); 602 603 do { 604 contentBounds = stepUtils.getContentBounds(stepIterator); 605 if (contentBounds) { 606 container = contentBounds.container; 607 if (odfUtils.isDowngradableSpaceElement(container)) { 608 lastChild = /**@type{!Node}*/(container.lastChild); 609 while(container.firstChild) { 610 // Merge contained space text node up to replace container 611 modifiedNodes.push(container.firstChild); 612 container.parentNode.insertBefore(container.firstChild, container); 613 } 614 container.parentNode.removeChild(container); 615 // Reset the iterator position to the same step it was just on, which was just to 616 // the right of a space 617 stepIterator.setPosition(lastChild, maxOffset(lastChild)); 618 stepIterator.roundToPreviousStep(); 619 } 620 } 621 stepsToUpgrade -= 1; 622 } while (stepsToUpgrade > 0 && stepIterator.nextStep()); 623 624 modifiedNodes.forEach(domUtils.normalizeTextNodes); 625 } 626 this.downgradeWhitespaces = downgradeWhitespaces; 627 628 /** 629 * Downgrades white space elements to normal spaces at the specified step, and one step 630 * to the right. 631 * 632 * @param {!number} step 633 * @return {undefined} 634 */ 635 this.downgradeWhitespacesAtPosition = function (step) { 636 var positionIterator = getIteratorAtPosition(step), 637 stepIterator = new core.StepIterator(filter, positionIterator); 638 639 downgradeWhitespaces(stepIterator); 640 }; 641 642 /** 643 * This function will return the Text node as well as the offset in that text node 644 * of the cursor. 645 * @param {!number} position 646 * @param {!string=} memberid 647 * @return {?{textNode: !Text, offset: !number}} 648 */ 649 this.getTextNodeAtStep = getTextNodeAtStep; 650 651 /** 652 * Returns the closest parent paragraph or root to the supplied container and offset 653 * @param {!Node} container 654 * @param {!number} offset 655 * @param {!Node} root 656 * 657 * @return {!Node} 658 */ 659 function paragraphOrRoot(container, offset, root) { 660 var node = container.childNodes.item(offset) || container, 661 paragraph = odfUtils.getParagraphElement(node); 662 if (paragraph && domUtils.containsNode(root, paragraph)) { 663 // Only return the paragraph if it is contained within the destination root 664 return /**@type{!Node}*/(paragraph); 665 } 666 // Otherwise the step filter should be contained within the supplied root 667 return root; 668 } 669 670 /** 671 * Iterates through all cursors and checks if they are in 672 * walkable positions; if not, move the cursor 1 filtered step backward 673 * which guarantees walkable state for all cursors, 674 * while keeping them inside the same root. An event will be raised for this cursor if it is moved 675 */ 676 this.fixCursorPositions = function () { 677 Object.keys(cursors).forEach(function (memberId) { 678 var cursor = cursors[memberId], 679 root = getRoot(cursor.getNode()), 680 rootFilter = self.createRootFilter(root), 681 subTree, 682 startPoint, 683 endPoint, 684 selectedRange, 685 cursorMoved = false; 686 687 selectedRange = cursor.getSelectedRange(); 688 subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, root); 689 startPoint = createStepIterator(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, 690 [filter, rootFilter], subTree); 691 692 if (!selectedRange.collapsed) { 693 subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, root); 694 endPoint = createStepIterator(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, 695 [filter, rootFilter], subTree); 696 } else { 697 endPoint = startPoint; 698 } 699 700 if (!startPoint.isStep() || !endPoint.isStep()) { 701 cursorMoved = true; 702 runtime.assert(startPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId); 703 selectedRange.setStart(startPoint.container(), startPoint.offset()); 704 runtime.assert(endPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId); 705 selectedRange.setEnd(endPoint.container(), endPoint.offset()); 706 } else if (startPoint.container() === endPoint.container() && startPoint.offset() === endPoint.offset()) { 707 // The range *should* be collapsed 708 if (!selectedRange.collapsed || cursor.getAnchorNode() !== cursor.getNode()) { 709 // It might not be collapsed if there are other unwalkable nodes (e.g., cursors) 710 // between the cursor and anchor nodes. In this case, force the cursor to collapse 711 cursorMoved = true; 712 selectedRange.setStart(startPoint.container(), startPoint.offset()); 713 selectedRange.collapse(true); 714 } 715 } 716 717 if (cursorMoved) { 718 cursor.setSelectedRange(selectedRange, cursor.hasForwardSelection()); 719 self.emit(ops.Document.signalCursorMoved, cursor); 720 } 721 }); 722 }; 723 724 /** 725 * This function returns the position in ODF world of the cursor of the member. 726 * @param {!string} memberid 727 * @return {!number} 728 */ 729 this.getCursorPosition = function (memberid) { 730 var cursor = cursors[memberid]; 731 return cursor ? stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0) : 0; 732 }; 733 734 /** 735 * This function returns the position and selection length in ODF world of 736 * the cursor of the member. 737 * position is always the number of steps from root node to the anchor node 738 * length is the number of steps from anchor node to focus node 739 * !IMPORTANT! length is a vector, and may be negative if the cursor selection 740 * is reversed (i.e., user clicked and dragged the cursor backwards) 741 * @param {!string} memberid 742 * @return {{position: !number, length: !number}} 743 */ 744 this.getCursorSelection = function (memberid) { 745 var cursor = cursors[memberid], 746 focusPosition = 0, 747 anchorPosition = 0; 748 if (cursor) { 749 focusPosition = stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0); 750 anchorPosition = stepsTranslator.convertDomPointToSteps(cursor.getAnchorNode(), 0); 751 } 752 return { 753 position: anchorPosition, 754 length: focusPosition - anchorPosition 755 }; 756 }; 757 /** 758 * @return {!core.PositionFilter} 759 */ 760 this.getPositionFilter = function () { 761 return filter; 762 }; 763 764 /** 765 * @return {!odf.OdfCanvas} 766 */ 767 this.getOdfCanvas = function () { 768 return odfCanvas; 769 }; 770 771 /** 772 * @return {!ops.Canvas} 773 */ 774 this.getCanvas = function () { 775 return odfCanvas; 776 }; 777 778 /** 779 * @return {!Element} 780 */ 781 this.getRootNode = getRootNode; 782 783 /** 784 * @param {!ops.Member} member 785 * @return {undefined} 786 */ 787 this.addMember = function (member) { 788 runtime.assert(members[member.getMemberId()] === undefined, "This member already exists"); 789 members[member.getMemberId()] = member; 790 }; 791 792 /** 793 * @param {!string} memberId 794 * @return {?ops.Member} 795 */ 796 this.getMember = function (memberId) { 797 return members.hasOwnProperty(memberId) ? members[memberId] : null; 798 }; 799 800 /** 801 * @param {!string} memberId 802 * @return {undefined} 803 */ 804 this.removeMember = function (memberId) { 805 delete members[memberId]; 806 }; 807 808 /** 809 * @param {!string} memberid 810 * @return {ops.OdtCursor} 811 */ 812 this.getCursor = function (memberid) { 813 return cursors[memberid]; 814 }; 815 816 /** 817 * @return {!Array.<string>} 818 */ 819 this.getMemberIds = function () { 820 var list = [], 821 /**@type{string}*/ 822 i; 823 for (i in cursors) { 824 if (cursors.hasOwnProperty(i)) { 825 list.push(cursors[i].getMemberId()); 826 } 827 } 828 return list; 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); 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 filter = new ops.TextPositionFilter(); 949 stepUtils = new odf.StepUtils(); 950 stepsTranslator = new ops.OdtStepsTranslator(getRootNode, createPositionIterator, 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