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