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