node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
977 lines (886 loc) • 39.1 kB
JavaScript
/**
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global Node, runtime, core, gui, ops, odf, NodeFilter*/
/**
* A filter that allows a position if it has the same closest
* whitelisted root as the specified 'anchor', which can be the cursor
* of the given memberid, or a given node
* @constructor
* @implements {core.PositionFilter}
* @param {!string|!Node} anchor
* @param {Object.<!ops.OdtCursor>} cursors
* @param {function(!Node):!Node} getRoot
*/
function RootFilter(anchor, cursors, getRoot) {
"use strict";
var /**@const*/
FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT,
/**@const*/
FILTER_REJECT = core.PositionFilter.FilterResult.FILTER_REJECT;
/**
* @param {!core.PositionIterator} iterator
* @return {!core.PositionFilter.FilterResult}
*/
this.acceptPosition = function (iterator) {
var node = iterator.container(),
anchorNode;
if (typeof anchor === "string") {
anchorNode = cursors[anchor].getNode();
} else {
anchorNode = anchor;
}
if (getRoot(node) === getRoot(anchorNode)) {
return FILTER_ACCEPT;
}
return FILTER_REJECT;
};
}
/**
* A document that keeps all data related to the mapped document.
* @constructor
* @implements {ops.Document}
* @implements {core.Destroyable}
* @param {!odf.OdfCanvas} odfCanvas
*/
ops.OdtDocument = function OdtDocument(odfCanvas) {
"use strict";
var self = this,
/**@type{!odf.StepUtils}*/
stepUtils,
odfUtils = odf.OdfUtils,
domUtils = core.DomUtils,
/**!Object.<!ops.OdtCursor>*/
cursors = {},
/**!Object.<!ops.Member>*/
members = {},
eventNotifier = new core.EventNotifier([
ops.Document.signalMemberAdded,
ops.Document.signalMemberUpdated,
ops.Document.signalMemberRemoved,
ops.Document.signalCursorAdded,
ops.Document.signalCursorRemoved,
ops.Document.signalCursorMoved,
ops.OdtDocument.signalParagraphChanged,
ops.OdtDocument.signalParagraphStyleModified,
ops.OdtDocument.signalCommonStyleCreated,
ops.OdtDocument.signalCommonStyleDeleted,
ops.OdtDocument.signalTableAdded,
ops.OdtDocument.signalOperationStart,
ops.OdtDocument.signalOperationEnd,
ops.OdtDocument.signalProcessingBatchStart,
ops.OdtDocument.signalProcessingBatchEnd,
ops.OdtDocument.signalUndoStackChanged,
ops.OdtDocument.signalStepsInserted,
ops.OdtDocument.signalStepsRemoved,
ops.OdtDocument.signalMetadataUpdated,
ops.OdtDocument.signalAnnotationAdded
]),
/**@const*/
NEXT = core.StepDirection.NEXT,
filter,
/**@type{!ops.OdtStepsTranslator}*/
stepsTranslator,
lastEditingOp,
unsupportedMetadataRemoved = false,
/**@const*/ SHOW_ALL = NodeFilter.SHOW_ALL,
blacklistedNodes = new gui.BlacklistNamespaceNodeFilter(["urn:webodf:names:cursor", "urn:webodf:names:editinfo"]),
odfTextBodyFilter = new gui.OdfTextBodyNodeFilter(),
defaultNodeFilter = new core.NodeFilterChain([blacklistedNodes, odfTextBodyFilter]);
/**
*
* @param {!Node} rootNode
* @return {!core.PositionIterator}
*/
function createPositionIterator(rootNode) {
return new core.PositionIterator(rootNode, SHOW_ALL, defaultNodeFilter, false);
}
this.createPositionIterator = createPositionIterator;
/**
* Return the office:text element of this document.
* @return {!Element}
*/
function getRootNode() {
var element = odfCanvas.odfContainer().getContentElement(),
localName = element && element.localName;
runtime.assert(localName === "text", "Unsupported content element type '" + localName + "' for OdtDocument");
return element;
}
/**
* Return the office:document element of this document.
* @return {!Element}
*/
this.getDocumentElement = function () {
return odfCanvas.odfContainer().rootElement;
};
this.cloneDocumentElement = function () {
var rootElement = self.getDocumentElement(),
annotationViewManager = odfCanvas.getAnnotationViewManager(),
initialDoc;
if (annotationViewManager) {
annotationViewManager.forgetAnnotations();
}
initialDoc = rootElement.cloneNode(true);
odfCanvas.refreshAnnotations();
// workaround AnnotationViewManager not fixing up cursor positions after creating the highlighting
self.fixCursorPositions();
return initialDoc;
};
/**
* @param {!Element} documentElement
*/
this.setDocumentElement = function (documentElement) {
var odfContainer = odfCanvas.odfContainer(),
rootNode;
eventNotifier.unsubscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted);
eventNotifier.unsubscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved);
// TODO Replace with a neater hack for reloading the Odt tree
// Once this is fixed, SelectionView.addOverlays can be removed
odfContainer.setRootElement(documentElement);
odfCanvas.setOdfContainer(odfContainer, true);
odfCanvas.refreshCSS();
rootNode = getRootNode();
stepsTranslator = new ops.OdtStepsTranslator(rootNode, createPositionIterator(rootNode), filter, 500);
eventNotifier.subscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted);
eventNotifier.subscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved);
};
/**
* @return {!Document}
*/
function getDOMDocument() {
return /**@type{!Document}*/(self.getDocumentElement().ownerDocument);
}
this.getDOMDocument = getDOMDocument;
/**
* @param {!Node} node
* @return {!boolean}
*/
function isRoot(node) {
if ((node.namespaceURI === odf.Namespaces.officens
&& node.localName === 'text'
) || (node.namespaceURI === odf.Namespaces.officens
&& node.localName === 'annotation')) {
return true;
}
return false;
}
/**
* @param {!Node} node
* @return {!Node}
*/
function getRoot(node) {
while (node && !isRoot(node)) {
node = /**@type{!Node}*/(node.parentNode);
}
return node;
}
this.getRootElement = getRoot;
/**
* Create a new StepIterator instance set to the defined position
*
* @param {!Node} container
* @param {!number} offset
* @param {!Array.<!core.PositionFilter>} filters Filter to apply to the iterator positions. If multiple
* iterators are provided, they will be combined in order using a PositionFilterChain.
* @param {!Node} subTree Subtree to search for step within. Generally a paragraph or document root. Choosing
* a smaller subtree allows iteration to end quickly if there are no walkable steps remaining in a particular
* direction. This can vastly improve performance.
*
* @return {!core.StepIterator}
*/
function createStepIterator(container, offset, filters, subTree) {
var positionIterator = createPositionIterator(subTree),
filterOrChain,
stepIterator;
if (filters.length === 1) {
filterOrChain = filters[0];
} else {
filterOrChain = new core.PositionFilterChain();
filters.forEach(filterOrChain.addFilter);
}
stepIterator = new core.StepIterator(filterOrChain, positionIterator);
stepIterator.setPosition(container, offset);
return stepIterator;
}
this.createStepIterator = createStepIterator;
/**
* Returns a PositionIterator instance at the
* specified starting position
* @param {!number} position
* @return {!core.PositionIterator}
*/
function getIteratorAtPosition(position) {
var iterator = createPositionIterator(getRootNode()),
point = stepsTranslator.convertStepsToDomPoint(position);
iterator.setUnfilteredPosition(point.node, point.offset);
return iterator;
}
this.getIteratorAtPosition = getIteratorAtPosition;
/**
* Converts the requested step number from root into the equivalent DOM node & offset
* pair. If the step is outside the bounds of the document, a RangeError will be thrown.
* @param {!number} step
* @return {!{node: !Node, offset: !number}}
*/
this.convertCursorStepToDomPoint = function (step) {
return stepsTranslator.convertStepsToDomPoint(step);
};
/**
* Rounds to the first step within the paragraph
* @param {!core.StepDirection} step
* @return {!boolean}
*/
function roundUp(step) {
return step === NEXT;
}
/**
* Converts a DOM node and offset pair to a cursor step. If a rounding direction is not supplied then
* the default is to round down to the previous step.
* @param {!Node} node
* @param {!number} offset
* @param {core.StepDirection=} roundDirection Whether to round down to the previous step or round up
* to the next step. The default value if unspecified is core.StepDirection.PREVIOUS
* @return {!number}
*/
this.convertDomPointToCursorStep = function (node, offset, roundDirection) {
var roundingFunc;
if(roundDirection === NEXT) {
roundingFunc = roundUp;
}
return stepsTranslator.convertDomPointToSteps(node, offset, roundingFunc);
};
/**
* @param {!{anchorNode: !Node, anchorOffset: !number, focusNode: !Node, focusOffset: !number}} selection
* @return {!{position: !number, length: number}}
*/
this.convertDomToCursorRange = function (selection) {
var point1,
point2;
point1 = stepsTranslator.convertDomPointToSteps(selection.anchorNode, selection.anchorOffset);
if (selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) {
point2 = point1;
} else {
point2 = stepsTranslator.convertDomPointToSteps(selection.focusNode, selection.focusOffset);
}
return {
position: point1,
length: point2 - point1
};
};
/**
* Convert a cursor range to a DOM range
* @param {!number} position
* @param {!number} length
* @return {!Range}
*/
this.convertCursorToDomRange = function (position, length) {
var range = getDOMDocument().createRange(),
point1,
point2;
point1 = stepsTranslator.convertStepsToDomPoint(position);
if (length) {
point2 = stepsTranslator.convertStepsToDomPoint(position + length);
if (length > 0) {
range.setStart(point1.node, point1.offset);
range.setEnd(point2.node, point2.offset);
} else {
range.setStart(point2.node, point2.offset);
range.setEnd(point1.node, point1.offset);
}
} else {
range.setStart(point1.node, point1.offset);
}
return range;
};
/**
* This function will iterate through positions allowed by the position
* iterator and count only the text positions. When the amount defined by
* offset has been counted, the Text node that that position is returned
* as well as the offset in that text node.
* Optionally takes a memberid of a cursor, to specifically return the
* text node positioned just behind that cursor.
* @param {!number} steps
* @param {!string=} memberid
* @return {?{textNode: !Text, offset: !number}}
*/
function getTextNodeAtStep(steps, memberid) {
var iterator = getIteratorAtPosition(steps),
node = iterator.container(),
lastTextNode,
nodeOffset = 0,
cursorNode = null,
text;
if (node.nodeType === Node.TEXT_NODE) {
// Iterator has stopped within an existing text node, to put that up as a possible target node
lastTextNode = /**@type{!Text}*/(node);
nodeOffset = /**@type{!number}*/(iterator.unfilteredDomOffset());
// Always cut in a new empty text node at the requested position.
// If this proves to be unnecessary, it will be cleaned up just before the return
// after all necessary cursor rearrangements have been performed
if (lastTextNode.length > 0) {
// The node + offset returned make up the boundary just to the right of the requested step
if (nodeOffset > 0) {
// In this case, after the split, the right of the requested step is just after the new node
lastTextNode = lastTextNode.splitText(nodeOffset);
}
lastTextNode.parentNode.insertBefore(getDOMDocument().createTextNode(""), lastTextNode);
lastTextNode = /**@type{!Text}*/(lastTextNode.previousSibling);
nodeOffset = 0;
}
} else {
// There is no text node at the current position, so insert a new one at the current position
lastTextNode = getDOMDocument().createTextNode("");
nodeOffset = 0;
node.insertBefore(lastTextNode, iterator.rightNode());
}
if (memberid) {
// DEPRECATED: This branch is no longer the recommended way of handling cursor movements DO NOT USE
// If the member cursor is as the requested position
if (cursors[memberid] && self.getCursorPosition(memberid) === steps) {
cursorNode = cursors[memberid].getNode();
// Then move the member's cursor after all adjacent cursors
while (cursorNode.nextSibling && cursorNode.nextSibling.localName === "cursor") {
// TODO this re-arrange logic will break if there are non-cursor elements in the way
// E.g., cursors occupy the same "step", but are on different sides of a span boundary
// This is currently avoided by calling fixCursorPositions after (almost) every op
// to re-arrange cursors together again
cursorNode.parentNode.insertBefore(cursorNode.nextSibling, cursorNode);
}
if (lastTextNode.length > 0 && lastTextNode.nextSibling !== cursorNode) {
// The last text node contains content but is not adjacent to the cursor
// This can't be moved, as moving it would move the text content around as well. Yikes!
// So, create a new text node to insert data into
lastTextNode = getDOMDocument().createTextNode('');
nodeOffset = 0;
}
// Keep the destination text node right next to the member's cursor, so inserted text pushes the cursor over
cursorNode.parentNode.insertBefore(lastTextNode, cursorNode);
}
} else {
// Move all cursors BEFORE the new text node. Any cursors occupying the requested position should not
// move when new text is added in the position
while (lastTextNode.nextSibling && lastTextNode.nextSibling.localName === "cursor") {
// TODO this re-arrange logic will break if there are non-cursor elements in the way
// E.g., cursors occupy the same "step", but are on different sides of a span boundary
// This is currently avoided by calling fixCursorPositions after (almost) every op
// to re-arrange cursors together again
lastTextNode.parentNode.insertBefore(lastTextNode.nextSibling, lastTextNode);
}
}
// After the above cursor adjustments, if the lastTextNode
// has a text node previousSibling, merge them and make the result the lastTextNode
while (lastTextNode.previousSibling && lastTextNode.previousSibling.nodeType === Node.TEXT_NODE) {
text = /**@type{!Text}*/(lastTextNode.previousSibling);
text.appendData(lastTextNode.data);
nodeOffset = text.length;
lastTextNode = text;
lastTextNode.parentNode.removeChild(lastTextNode.nextSibling);
}
// Empty text nodes can be left on either side of the split operations that have occurred
while (lastTextNode.nextSibling && lastTextNode.nextSibling.nodeType === Node.TEXT_NODE) {
text = /**@type{!Text}*/(lastTextNode.nextSibling);
lastTextNode.appendData(text.data);
lastTextNode.parentNode.removeChild(text);
}
return {textNode: lastTextNode, offset: nodeOffset };
}
/**
* Called after an operation is executed, this
* function will check if the operation is an
* 'edit', and in that case will update the
* document's metadata, such as dc:creator,
* meta:editing-cycles, and dc:creator.
* @param {!ops.Operation} op
*/
function handleOperationExecuted(op) {
var opspec = op.spec(),
memberId = opspec.memberid,
date = new Date(opspec.timestamp).toISOString(),
odfContainer = odfCanvas.odfContainer(),
/**@type{!{setProperties: !Object, removedProperties: ?Array.<!string>}}*/
changedMetadata,
fullName;
// If the operation is an edit (that changes the
// ODF that will be saved), then update metadata.
if (op.isEdit) {
fullName = self.getMember(memberId).getProperties().fullName;
odfContainer.setMetadata({
"dc:creator": fullName,
"dc:date": date
}, null);
changedMetadata = {
setProperties: {
"dc:creator": fullName,
"dc:date": date
},
removedProperties: []
};
// If no previous op was found in this session,
// then increment meta:editing-cycles by 1.
if (!lastEditingOp) {
changedMetadata.setProperties["meta:editing-cycles"] = odfContainer.incrementEditingCycles();
// Remove certain metadata fields that
// should be updated as soon as edits happen,
// but cannot be because we don't support those yet.
if (!unsupportedMetadataRemoved) {
odfContainer.setMetadata(null, [
"meta:editing-duration",
"meta:document-statistic"
]);
}
}
lastEditingOp = op;
self.emit(ops.OdtDocument.signalMetadataUpdated, changedMetadata);
}
}
/**
* Upgrades literal whitespaces (' ') to <text:s> </text:s>,
* when given a textNode containing the whitespace and an offset
* indicating the location of the whitespace in it.
* @param {!Text} textNode
* @param {!number} offset
* @return {!Element}
*/
function upgradeWhitespaceToElement(textNode, offset) {
runtime.assert(textNode.data[offset] === ' ', "upgradeWhitespaceToElement: textNode.data[offset] should be a literal space");
var space = textNode.ownerDocument.createElementNS(odf.Namespaces.textns, 'text:s'),
container = textNode.parentNode,
adjacentNode = textNode;
space.appendChild(textNode.ownerDocument.createTextNode(' '));
if (textNode.length === 1) {
// The space is the only element in this node. Can simply replace it
container.replaceChild(space, textNode);
} else {
textNode.deleteData(offset, 1);
if (offset > 0) { // Don't create an empty text node if the offset is 0...
if (offset < textNode.length) {
// Don't split if offset === textNode.length as this would add an empty text node after
textNode.splitText(offset);
}
adjacentNode = textNode.nextSibling;
}
container.insertBefore(space, adjacentNode);
}
return space;
}
/**
* @param {!number} step
* @return {undefined}
*/
function upgradeWhitespacesAtPosition(step) {
var positionIterator = getIteratorAtPosition(step),
stepIterator = new core.StepIterator(filter, positionIterator),
contentBounds,
/**@type{?Node}*/
container,
offset,
stepsToUpgrade = 2;
// The step passed into this function is the point of change. Need to
// upgrade whitespace to the left of the current step, and to the left of the next step
runtime.assert(stepIterator.isStep(), "positionIterator is not at a step (requested step: " + step + ")");
do {
contentBounds = stepUtils.getContentBounds(stepIterator);
if (contentBounds) {
container = contentBounds.container;
offset = contentBounds.startOffset;
if (container.nodeType === Node.TEXT_NODE
&& odfUtils.isSignificantWhitespace(/**@type{!Text}*/(container), offset)) {
container = upgradeWhitespaceToElement(/**@type{!Text}*/(container), offset);
// Reset the iterator position to the same step it was just on, which was just to
// the right of a space
stepIterator.setPosition(container, container.childNodes.length);
stepIterator.roundToPreviousStep();
}
}
stepsToUpgrade -= 1;
} while (stepsToUpgrade > 0 && stepIterator.nextStep());
}
/**
* Upgrades any significant whitespace at the requested step, and one step right of the given
* position to space elements.
* @param {!number} step
* @return {undefined}
*/
this.upgradeWhitespacesAtPosition = upgradeWhitespacesAtPosition;
/**
* Returns the maximum available offset for the specified node.
* @param {!Node} node
* @return {!number}
*/
function maxOffset(node) {
return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length;
}
/**
* Downgrades white space elements to normal spaces at the step iterators current step, and one step
* to the right.
*
* @param {!core.StepIterator} stepIterator
* @return {undefined}
*/
function downgradeWhitespaces(stepIterator) {
var contentBounds,
/**@type{!Node}*/
container,
modifiedNodes = [],
lastChild,
stepsToUpgrade = 2;
// The step passed into this function is the point of change. Need to
// downgrade whitespace to the left of the current step, and to the left of the next step
runtime.assert(stepIterator.isStep(), "positionIterator is not at a step");
do {
contentBounds = stepUtils.getContentBounds(stepIterator);
if (contentBounds) {
container = contentBounds.container;
if (odfUtils.isDowngradableSpaceElement(container)) {
lastChild = /**@type{!Node}*/(container.lastChild);
while(container.firstChild) {
// Merge contained space text node up to replace container
modifiedNodes.push(container.firstChild);
container.parentNode.insertBefore(container.firstChild, container);
}
container.parentNode.removeChild(container);
// Reset the iterator position to the same step it was just on, which was just to
// the right of a space
stepIterator.setPosition(lastChild, maxOffset(lastChild));
stepIterator.roundToPreviousStep();
}
}
stepsToUpgrade -= 1;
} while (stepsToUpgrade > 0 && stepIterator.nextStep());
modifiedNodes.forEach(domUtils.normalizeTextNodes);
}
this.downgradeWhitespaces = downgradeWhitespaces;
/**
* Downgrades white space elements to normal spaces at the specified step, and one step
* to the right.
*
* @param {!number} step
* @return {undefined}
*/
this.downgradeWhitespacesAtPosition = function (step) {
var positionIterator = getIteratorAtPosition(step),
stepIterator = new core.StepIterator(filter, positionIterator);
downgradeWhitespaces(stepIterator);
};
/**
* This function will return the Text node as well as the offset in that text node
* of the cursor.
* @param {!number} position
* @param {!string=} memberid
* @return {?{textNode: !Text, offset: !number}}
*/
this.getTextNodeAtStep = getTextNodeAtStep;
/**
* Returns the closest parent paragraph or root to the supplied container and offset
* @param {!Node} container
* @param {!number} offset
* @param {!Node} root
*
* @return {!Node}
*/
function paragraphOrRoot(container, offset, root) {
var node = container.childNodes.item(offset) || container,
paragraph = odfUtils.getParagraphElement(node);
if (paragraph && domUtils.containsNode(root, paragraph)) {
// Only return the paragraph if it is contained within the destination root
return /**@type{!Node}*/(paragraph);
}
// Otherwise the step filter should be contained within the supplied root
return root;
}
/**
* Iterates through all cursors and checks if they are in
* walkable positions; if not, move the cursor 1 filtered step backward
* which guarantees walkable state for all cursors,
* while keeping them inside the same root. An event will be raised for this cursor if it is moved
*/
this.fixCursorPositions = function () {
Object.keys(cursors).forEach(function (memberId) {
var cursor = cursors[memberId],
root = getRoot(cursor.getNode()),
rootFilter = self.createRootFilter(root),
subTree,
startPoint,
endPoint,
selectedRange,
cursorMoved = false;
selectedRange = cursor.getSelectedRange();
subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, root);
startPoint = createStepIterator(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset,
[filter, rootFilter], subTree);
if (!selectedRange.collapsed) {
subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, root);
endPoint = createStepIterator(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset,
[filter, rootFilter], subTree);
} else {
endPoint = startPoint;
}
if (!startPoint.isStep() || !endPoint.isStep()) {
cursorMoved = true;
runtime.assert(startPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId);
selectedRange.setStart(startPoint.container(), startPoint.offset());
runtime.assert(endPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId);
selectedRange.setEnd(endPoint.container(), endPoint.offset());
} else if (startPoint.container() === endPoint.container() && startPoint.offset() === endPoint.offset()) {
// The range *should* be collapsed
if (!selectedRange.collapsed || cursor.getAnchorNode() !== cursor.getNode()) {
// It might not be collapsed if there are other unwalkable nodes (e.g., cursors)
// between the cursor and anchor nodes. In this case, force the cursor to collapse
cursorMoved = true;
selectedRange.setStart(startPoint.container(), startPoint.offset());
selectedRange.collapse(true);
}
}
if (cursorMoved) {
cursor.setSelectedRange(selectedRange, cursor.hasForwardSelection());
self.emit(ops.Document.signalCursorMoved, cursor);
}
});
};
/**
* This function returns the position in ODF world of the cursor of the member.
* @param {!string} memberid
* @return {!number}
*/
this.getCursorPosition = function (memberid) {
var cursor = cursors[memberid];
return cursor ? stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0) : 0;
};
/**
* This function returns the position and selection length in ODF world of
* the cursor of the member.
* position is always the number of steps from root node to the anchor node
* length is the number of steps from anchor node to focus node
* !IMPORTANT! length is a vector, and may be negative if the cursor selection
* is reversed (i.e., user clicked and dragged the cursor backwards)
* @param {!string} memberid
* @return {{position: !number, length: !number}}
*/
this.getCursorSelection = function (memberid) {
var cursor = cursors[memberid],
focusPosition = 0,
anchorPosition = 0;
if (cursor) {
focusPosition = stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0);
anchorPosition = stepsTranslator.convertDomPointToSteps(cursor.getAnchorNode(), 0);
}
return {
position: anchorPosition,
length: focusPosition - anchorPosition
};
};
/**
* @return {!core.PositionFilter}
*/
this.getPositionFilter = function () {
return filter;
};
/**
* @return {!odf.OdfCanvas}
*/
this.getOdfCanvas = function () {
return odfCanvas;
};
/**
* @return {!ops.Canvas}
*/
this.getCanvas = function () {
return odfCanvas;
};
/**
* @return {!Element}
*/
this.getRootNode = getRootNode;
/**
* @param {!ops.Member} member
* @return {undefined}
*/
this.addMember = function (member) {
runtime.assert(members[member.getMemberId()] === undefined, "This member already exists");
members[member.getMemberId()] = member;
};
/**
* @param {!string} memberId
* @return {?ops.Member}
*/
this.getMember = function (memberId) {
return members.hasOwnProperty(memberId) ? members[memberId] : null;
};
/**
* @param {!string} memberId
* @return {undefined}
*/
this.removeMember = function (memberId) {
delete members[memberId];
};
/**
* @param {!string} memberid
* @return {ops.OdtCursor}
*/
this.getCursor = function (memberid) {
return cursors[memberid];
};
/**
* @param {!string} memberid
* @return {!boolean}
*/
this.hasCursor = function (memberid) {
return cursors.hasOwnProperty(memberid);
};
/**
* @return {!Array.<string>}
*/
this.getMemberIds = function () {
return Object.keys(members);
};
/**
* Adds the specified cursor to the ODT document. The cursor will be collapsed
* to the first available cursor position in the document.
* @param {!ops.OdtCursor} cursor
* @return {undefined}
*/
this.addCursor = function (cursor) {
runtime.assert(Boolean(cursor), "OdtDocument::addCursor without cursor");
var memberid = cursor.getMemberId(),
initialSelection = self.convertCursorToDomRange(0, 0);
runtime.assert(typeof memberid === "string", "OdtDocument::addCursor has cursor without memberid");
runtime.assert(!cursors[memberid], "OdtDocument::addCursor is adding a duplicate cursor with memberid " + memberid);
cursor.setSelectedRange(initialSelection, true);
cursors[memberid] = cursor;
};
/**
* @param {!string} memberid
* @return {!boolean}
*/
this.removeCursor = function (memberid) {
var cursor = cursors[memberid];
if (cursor) {
cursor.removeFromDocument();
delete cursors[memberid];
self.emit(ops.Document.signalCursorRemoved, memberid);
return true;
}
return false;
};
/**
* Moves the cursor/selection of a given memberid to the
* given position+length combination and adopts the given
* selectionType.
* It is the caller's responsibility to decide if and when
* to subsequently fire signalCursorMoved.
* @param {!string} memberid
* @param {!number} position
* @param {!number} length
* @param {!string=} selectionType
* @return {undefined}
*/
this.moveCursor = function (memberid, position, length, selectionType) {
var cursor = cursors[memberid],
selectionRange = self.convertCursorToDomRange(position, length);
if (cursor) {
cursor.setSelectedRange(selectionRange, length >= 0);
cursor.setSelectionType(selectionType || ops.OdtCursor.RangeSelection);
}
};
/**
* @return {!odf.Formatting}
*/
this.getFormatting = function () {
return odfCanvas.getFormatting();
};
/**
* @param {!string} eventid
* @param {*} args
* @return {undefined}
*/
this.emit = function (eventid, args) {
eventNotifier.emit(eventid, args);
};
/**
* @param {!string} eventid
* @param {!Function} cb
* @return {undefined}
*/
this.subscribe = function (eventid, cb) {
eventNotifier.subscribe(eventid, cb);
};
/**
* @param {!string} eventid
* @param {!Function} cb
* @return {undefined}
*/
this.unsubscribe = function (eventid, cb) {
eventNotifier.unsubscribe(eventid, cb);
};
/**
* @param {string|!Node} inputMemberId
* @return {!RootFilter}
*/
this.createRootFilter = function (inputMemberId) {
return new RootFilter(inputMemberId, cursors, getRoot);
};
/**
* @param {!function(!Object=)} callback, passing an error object in case of error
* @return {undefined}
*/
this.close = function (callback) {
// TODO: check if anything needs to be cleaned up
callback();
};
/**
* @param {!function(!Error=)} callback, passing an error object in case of error
* @return {undefined}
*/
this.destroy = function (callback) {
callback();
};
/**
* @return {undefined}
*/
function init() {
var rootNode = getRootNode();
filter = new ops.TextPositionFilter();
stepUtils = new odf.StepUtils();
stepsTranslator = new ops.OdtStepsTranslator(rootNode, createPositionIterator(rootNode), filter, 500);
eventNotifier.subscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted);
eventNotifier.subscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved);
eventNotifier.subscribe(ops.OdtDocument.signalOperationEnd, handleOperationExecuted);
eventNotifier.subscribe(ops.OdtDocument.signalProcessingBatchEnd, core.Task.processTasks);
}
init();
};
/**@const*/ops.OdtDocument.signalParagraphChanged = "paragraph/changed";
/**@const*/ops.OdtDocument.signalTableAdded = "table/added";
/**@const*/ops.OdtDocument.signalCommonStyleCreated = "style/created";
/**@const*/ops.OdtDocument.signalCommonStyleDeleted = "style/deleted";
/**@const*/ops.OdtDocument.signalParagraphStyleModified = "paragraphstyle/modified";
/**@const*/ops.OdtDocument.signalOperationStart = "operation/start";
/**@const*/ops.OdtDocument.signalOperationEnd = "operation/end";
/**@const*/ops.OdtDocument.signalProcessingBatchStart = "router/batchstart";
/**@const*/ops.OdtDocument.signalProcessingBatchEnd = "router/batchend";
/**@const*/ops.OdtDocument.signalUndoStackChanged = "undo/changed";
/**@const*/ops.OdtDocument.signalStepsInserted = "steps/inserted";
/**@const*/ops.OdtDocument.signalStepsRemoved = "steps/removed";
/**@const*/ops.OdtDocument.signalMetadataUpdated = "metadata/updated";
/**@const*/ops.OdtDocument.signalAnnotationAdded = "annotation/added";
// vim:expandtab