1 /**
  2  * Copyright (C) 2012 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 core, ops, gui, runtime*/
 26 
 27 /**
 28  * @class
 29  * A cursor is a dom node that visually represents a cursor in a DOM tree.
 30  * It should stay synchronized with the selection in the document. When
 31  * there is only one collapsed selection range, a cursor should be shown at
 32  * that point.
 33  *
 34  * Putting the cursor in the DOM tree modifies the DOM, so care should be taken
 35  * to keep the selection consistent. If e.g. a selection is drawn over the
 36  * cursor, and the cursor is updated to the selection, the cursor is removed
 37  * from the DOM because the selection is not collapsed. This means that the
 38  * offsets of the selection may have to be changed.
 39  *
 40  * When the selection is collapsed, the cursor is placed after the point of the
 41  * selection and the selection will stay valid. However, if the cursor was
 42  * placed in the DOM tree and was counted in the offset, the offset in the
 43  * selection should be decreased by one.
 44  *
 45  * Even when the selection allows for a cursor, it might be desireable to hide
 46  * the cursor by not letting it be part of the DOM.
 47  *
 48  * @constructor
 49  * @param {!string} memberId The memberid this cursor is assigned to
 50  * @param {!ops.Document} document The document in which the cursor is placed
 51  */
 52 ops.OdtCursor = function OdtCursor(memberId, document) {
 53     "use strict";
 54     var self = this,
 55         validSelectionTypes = {},
 56         selectionType,
 57         /**@type{!gui.SelectionMover}*/
 58         selectionMover,
 59         /**@type{!core.Cursor}*/
 60         cursor,
 61         events = new core.EventNotifier([ops.OdtCursor.signalCursorUpdated]);
 62 
 63     /**
 64      * Remove the cursor from the document
 65      * @return {undefined}
 66      */
 67     this.removeFromDocument = function () {
 68         // TODO: find out if nodeAfterCursor, textNodeIncrease need to be dealt with in any way
 69         cursor.remove();
 70     };
 71 
 72     /**
 73      * Subscribe to cursor update events.
 74      *
 75      * The update event called whenever the cursor is moved around manually.
 76      * @param {!string} eventid
 77      * @param {!Function} cb
 78      */
 79     this.subscribe = function (eventid, cb) {
 80         events.subscribe(eventid, cb);
 81     };
 82 
 83     /**
 84      * Unsubscribe from cursor events
 85      * @param {!string} eventid
 86      * @param {!Function} cb
 87      */
 88     this.unsubscribe = function (eventid, cb) {
 89         events.unsubscribe(eventid, cb);
 90     };
 91 
 92     /**
 93      * @return {!gui.StepCounter}
 94      */
 95     this.getStepCounter = function () {
 96         return selectionMover.getStepCounter();
 97     };
 98     /**
 99      * Obtain the memberid the cursor is assigned to.
100      * @return {string}
101      */
102     this.getMemberId = function () {
103         return memberId;
104     };
105     /**
106      * Obtain the node representing the cursor.
107      * @return {!Element}
108      */
109     this.getNode = function () {
110         return cursor.getNode();
111     };
112     /**
113      * Obtain the node representing the selection start point.
114      * If a 0-length range is selected (e.g., by clicking without
115      * dragging),, this will return the exact same node as getNode
116      * @return {!Element}
117      */
118     this.getAnchorNode = function () {
119         return cursor.getAnchorNode();
120     };
121     /**
122      * Obtain the currently selected range to which the cursor corresponds.
123      * @return {!Range}
124      */
125     this.getSelectedRange = function () {
126         return cursor.getSelectedRange();
127     };
128     /** Set the given range as the selected range for this cursor
129      * @param {!Range} range,
130      * @param {boolean} isForwardSelection
131      * @return {undefined}
132      */
133     this.setSelectedRange = function (range, isForwardSelection) {
134         cursor.setSelectedRange(range, isForwardSelection);
135         events.emit(ops.OdtCursor.signalCursorUpdated, self);
136     };
137     /**
138      * Returns if the selection of this cursor has the
139      * same direction as the direction of the range
140      * @return {boolean}
141      */
142     this.hasForwardSelection = function () {
143         return cursor.hasForwardSelection();
144     };
145     /**
146      * Obtain the document to which the cursor corresponds.
147      * @return {!ops.Document}
148      */
149     this.getDocument = function () {
150         return document;
151     };
152 
153     /**
154      * Gets the current selection type.
155      * @return {!string}
156      */
157     this.getSelectionType = function () {
158         return selectionType;
159     };
160 
161     /**
162      * Sets the current selection type to the given value.
163      * @param {!string} value
164      * @return {undefined}
165      */
166     this.setSelectionType = function (value) {
167         if (validSelectionTypes.hasOwnProperty(value)) {
168             selectionType = value;
169         } else {
170             runtime.log("Invalid selection type: " + value);
171         }
172     };
173 
174     /**
175      * Reset selection type to default.
176      * @return {undefined}
177      */
178     this.resetSelectionType = function () {
179         self.setSelectionType(ops.OdtCursor.RangeSelection);
180     };
181 
182     function init() {
183         cursor = new core.Cursor(document.getDOMDocument(), memberId);
184         selectionMover = new gui.SelectionMover(cursor, document.getRootNode());
185 
186         validSelectionTypes[ops.OdtCursor.RangeSelection] = true;
187         validSelectionTypes[ops.OdtCursor.RegionSelection] = true;
188         self.resetSelectionType();
189     }
190 
191     init();
192 };
193 
194 /**@const
195    @type {!string} */
196 ops.OdtCursor.RangeSelection = 'Range';
197 /**@const
198    @type {!string} */
199 ops.OdtCursor.RegionSelection = 'Region';
200 /**@const
201  @type {!string} */
202 ops.OdtCursor.signalCursorUpdated = "cursorUpdated";
203