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