node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
223 lines (212 loc) • 8.56 kB
JavaScript
/**
* Copyright (C) 2012 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, core, ops, runtime*/
/**
* @class
* A cursor is a dom node that visually represents a cursor in a DOM tree.
* It should stay synchronized with the selection in the document. When
* there is only one collapsed selection range, a cursor should be shown at
* that point.
*
* Putting the cursor in the DOM tree modifies the DOM, so care should be taken
* to keep the selection consistent. If e.g. a selection is drawn over the
* cursor, and the cursor is updated to the selection, the cursor is removed
* from the DOM because the selection is not collapsed. This means that the
* offsets of the selection may have to be changed.
*
* When the selection is collapsed, the cursor is placed after the point of the
* selection and the selection will stay valid. However, if the cursor was
* placed in the DOM tree and was counted in the offset, the offset in the
* selection should be decreased by one.
*
* Even when the selection allows for a cursor, it might be desireable to hide
* the cursor by not letting it be part of the DOM.
*
* @constructor
* @param {!Document} document The DOM document in which the cursor is placed
* @param {!string} memberId The memberid this cursor is assigned to
*/
core.Cursor = function Cursor(document, memberId) {
"use strict";
var cursorns = 'urn:webodf:names:cursor',
/**@type{!Element}*/
cursorNode = document.createElementNS(cursorns, 'cursor'),
/**@type{!Element}*/
anchorNode = document.createElementNS(cursorns, 'anchor'),
forwardSelection,
recentlyModifiedNodes = [],
/**@type{!Range}*/
selectedRange = /**@type{!Range}*/(document.createRange()),
isCollapsed,
domUtils = core.DomUtils;
/**
* Split a text node and put the cursor into it.
* @param {!Node} node
* @param {!Text} container
* @param {!number} offset
* @return {undefined}
*/
function putIntoTextNode(node, container, offset) {
runtime.assert(Boolean(container), "putCursorIntoTextNode: invalid container");
var parent = container.parentNode;
runtime.assert(Boolean(parent), "putCursorIntoTextNode: container without parent");
runtime.assert(offset >= 0 && offset <= container.length, "putCursorIntoTextNode: offset is out of bounds");
if (offset === 0) {
parent.insertBefore(node, container);
} else if (offset === container.length) {
parent.insertBefore(node, container.nextSibling);
} else {
container.splitText(offset);
parent.insertBefore(node, container.nextSibling);
}
}
/**
* Remove the cursor from the tree.
* @param {!Element} node
*/
function removeNode(node) {
if (node.parentNode) {
recentlyModifiedNodes.push(node.previousSibling);
recentlyModifiedNodes.push(node.nextSibling);
node.parentNode.removeChild(node);
}
}
/**
* Put the cursor at a particular position.
* @param {!Node} node
* @param {!Node} container
* @param {!number} offset
* @return {undefined}
*/
function putNode(node, container, offset) {
if (container.nodeType === Node.TEXT_NODE) {
putIntoTextNode(node, /**@type{!Text}*/(container), offset);
} else if (container.nodeType === Node.ELEMENT_NODE) {
container.insertBefore(node, container.childNodes.item(offset));
}
recentlyModifiedNodes.push(node.previousSibling);
recentlyModifiedNodes.push(node.nextSibling);
}
/**
* Gets the earliest selection node in the document
* @return {!Node}
*/
function getStartNode() {
return forwardSelection ? anchorNode : cursorNode;
}
/**
* Gets the latest selection node in the document
* @return {!Node}
*/
function getEndNode() {
return forwardSelection ? cursorNode : anchorNode;
}
/**
* Obtain the node representing the cursor. This is
* the selection end point
* @return {!Element}
*/
this.getNode = function () {
return cursorNode;
};
/**
* Obtain the node representing the selection start point.
* If a 0-length range is selected (e.g., by clicking without
* dragging),, this will return the exact same node as getNode
* @return {!Element}
*/
this.getAnchorNode = function () {
return anchorNode.parentNode ? anchorNode : cursorNode;
};
/**
* Obtain the selection to which the cursor corresponds.
* @return {!Range}
*/
this.getSelectedRange = function () {
if (isCollapsed) {
selectedRange.setStartBefore(cursorNode);
selectedRange.collapse(true);
} else {
selectedRange.setStartAfter(getStartNode());
selectedRange.setEndBefore(getEndNode());
}
return selectedRange;
};
/**
* Synchronize the cursor to a specific range
* If there is a single collapsed selection range, the cursor will be placed
* there. If not, the cursor will be removed from the document tree.
* @param {!Range} range
* @param {boolean=} isForwardSelection Set to true to indicate the direction of the
* range is startContainer => endContainer. This should be false if
* the user creates a selection that ends before it starts in the document (i.e.,
* drags the range backwards from the start point)
* @return {undefined}
*/
this.setSelectedRange = function (range, isForwardSelection) {
if (selectedRange && selectedRange !== range) {
selectedRange.detach();
}
selectedRange = range;
forwardSelection = isForwardSelection !== false;
isCollapsed = range.collapsed;
// TODO the nodes need to be added and removed in the right order to preserve the range
if (range.collapsed) {
removeNode(anchorNode);
removeNode(cursorNode);
putNode(cursorNode, /**@type {!Node}*/(range.startContainer), range.startOffset);
} else {
removeNode(anchorNode);
removeNode(cursorNode);
// putting in the end node first eliminates the chance the position of the start node is destroyed
putNode(getEndNode(), /**@type {!Node}*/(range.endContainer), range.endOffset);
putNode(getStartNode(), /**@type {!Node}*/(range.startContainer), range.startOffset);
}
recentlyModifiedNodes.forEach(domUtils.normalizeTextNodes);
recentlyModifiedNodes.length = 0;
};
/**
* Returns if the selection of this cursor has the
* same direction as the direction of the range
* @return {boolean}
*/
this.hasForwardSelection = function () {
return forwardSelection;
};
/**
* Remove the cursor from the document tree.
* @return {undefined}
*/
this.remove = function () {
removeNode(cursorNode);
recentlyModifiedNodes.forEach(domUtils.normalizeTextNodes);
recentlyModifiedNodes.length = 0;
};
function init() {
// mark cursornode with memberid
cursorNode.setAttributeNS(cursorns, "memberId", memberId);
anchorNode.setAttributeNS(cursorns, "memberId", memberId);
}
init();
};