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 runtime, core, gui, ops*/
 26 
 27 
 28 /**
 29  * The caret manager is responsible for creating a caret as UI representation
 30  * of a member's cursor.
 31  * If the caret is for the local member, then the manager will control the
 32  * caret's current focus, and ensure the caret stays visible after every local
 33  * operation.
 34  * @constructor
 35  * @implements {core.Destroyable}
 36  * @param {!gui.SessionController} sessionController
 37  */
 38 gui.CaretManager = function CaretManager(sessionController) {
 39     "use strict";
 40     var /**@type{!Object.<string,!gui.Caret>}*/
 41         carets = {},
 42         window = runtime.getWindow(),
 43         ensureCaretVisibleTimeoutId,
 44         scrollIntoViewScheduled = false;
 45 
 46     /**
 47      * @param {!string} memberId
 48      * @return {?gui.Caret}
 49      */
 50     function getCaret(memberId) {
 51         return carets.hasOwnProperty(memberId) ? carets[memberId] : null;
 52     }
 53 
 54     /**
 55      * @return {!Array.<!gui.Caret>}
 56      */
 57     function getCarets() {
 58         return Object.keys(carets).map(function (memberid) {
 59             return carets[memberid];
 60         });
 61     }
 62 
 63     /**
 64      * @param {!string} memberId
 65      * @return {undefined}
 66      */
 67     function removeCaret(memberId) {
 68         var caret = carets[memberId];
 69         if (caret) {
 70             // Remove the caret before destroying it in case the destroy function causes new window/webodf events to be
 71             // triggered. This ensures the caret can't receive any new events once destroy has been invoked
 72             delete carets[memberId];
 73             if (memberId === sessionController.getInputMemberId()) {
 74                 sessionController.getEventManager().unsubscribe("compositionupdate", caret.handleUpdate);
 75             }
 76             /*jslint emptyblock:true*/
 77             caret.destroy(function() {});
 78             /*jslint emptyblock:false*/
 79         }
 80     }
 81 
 82     /**
 83      * @param {!ops.OdtCursor} cursor
 84      * @return {undefined}
 85      */
 86     function refreshLocalCaretBlinking(cursor) {
 87         var caret, memberId = cursor.getMemberId();
 88 
 89         if (memberId === sessionController.getInputMemberId()) {
 90             caret = getCaret(memberId);
 91             if (caret) {
 92                 caret.refreshCursorBlinking();
 93             }
 94         }
 95     }
 96 
 97     function executeEnsureCaretVisible() {
 98         var caret = getCaret(sessionController.getInputMemberId());
 99         scrollIntoViewScheduled = false;
100         if (caret) {
101             // Just in case CaretManager was destroyed whilst waiting for the timeout to elapse
102             caret.ensureVisible();
103         }
104     }
105 
106     function scheduleCaretVisibilityCheck() {
107         var caret = getCaret(sessionController.getInputMemberId());
108         if (caret) {
109             caret.handleUpdate(); // This is really noticeable if delayed. Calculate the cursor size immediately
110             if (!scrollIntoViewScheduled) {
111                 scrollIntoViewScheduled = true;
112                 // Delay the actual scrolling just in case there are a batch of
113                 // operations being performed. 50ms is close enough to "instant"
114                 // that the user won't notice the delay here.
115                 ensureCaretVisibleTimeoutId = runtime.setTimeout(executeEnsureCaretVisible, 50);
116             }
117         }
118     }
119 
120     /**
121      * @param {!{memberId:string}} info
122      * @return {undefined}
123      */
124     function ensureLocalCaretVisible(info) {
125         if (info.memberId === sessionController.getInputMemberId()) {
126             // on member edit actions ensure visibility of cursor
127             scheduleCaretVisibilityCheck();
128         }
129     }
130 
131     /**
132      * @return {undefined}
133      */
134     function focusLocalCaret() {
135         var caret = getCaret(sessionController.getInputMemberId());
136         if (caret) {
137             caret.setFocus();
138         }
139     }
140 
141     /**
142      * @return {undefined}
143      */
144     function blurLocalCaret() {
145         var caret = getCaret(sessionController.getInputMemberId());
146         if (caret) {
147             caret.removeFocus();
148         }
149     }
150 
151     /**
152      * @return {undefined}
153      */
154     function showLocalCaret() {
155         var caret = getCaret(sessionController.getInputMemberId());
156         if (caret) {
157             caret.show();
158         }
159     }
160 
161     /**
162      * @return {undefined}
163      */
164     function hideLocalCaret() {
165         var caret = getCaret(sessionController.getInputMemberId());
166         if (caret) {
167             caret.hide();
168         }
169     }
170 
171     /**
172      * @param {!ops.OdtCursor} cursor
173      * @param {!boolean} caretAvatarInitiallyVisible  Set to false to hide the associated avatar
174      * @param {!boolean} blinkOnRangeSelect  Specify that the caret should blink if a non-collapsed range is selected
175      * @return {!gui.Caret}
176      */
177     this.registerCursor = function (cursor, caretAvatarInitiallyVisible, blinkOnRangeSelect) {
178         var memberid = cursor.getMemberId(),
179             caret = new gui.Caret(cursor, caretAvatarInitiallyVisible, blinkOnRangeSelect),
180             eventManager = sessionController.getEventManager();
181 
182         carets[memberid] = caret;
183 
184         // if local input member, then let controller listen on caret span
185         if (memberid === sessionController.getInputMemberId()) {
186             runtime.log("Starting to track input on new cursor of " + memberid);
187 
188             // wire up the cursor update to caret visibility update
189             cursor.subscribe(ops.OdtCursor.signalCursorUpdated, scheduleCaretVisibilityCheck);
190             eventManager.subscribe("compositionupdate", caret.handleUpdate);
191             // Add event trap as an overlay element to the caret
192             caret.setOverlayElement(eventManager.getEventTrap());
193         } else {
194             cursor.subscribe(ops.OdtCursor.signalCursorUpdated, caret.handleUpdate);
195         }
196 
197         return caret;
198     };
199 
200     /**
201      * @param {!string} memberId
202      * @return {?gui.Caret}
203      */
204     this.getCaret = getCaret;
205 
206     /**
207      * @return {!Array.<!gui.Caret>}
208      */
209     this.getCarets = getCarets;
210 
211     /**
212      * @param {!function(!Error=)} callback, passing an error object in case of error
213      * @return {undefined}
214      */
215     this.destroy = function (callback) {
216         var odtDocument = sessionController.getSession().getOdtDocument(),
217             eventManager = sessionController.getEventManager(),
218             caretCleanup = getCarets().map(function(caret) { return caret.destroy; });
219 
220         runtime.clearTimeout(ensureCaretVisibleTimeoutId);
221         odtDocument.unsubscribe(ops.OdtDocument.signalParagraphChanged, ensureLocalCaretVisible);
222         odtDocument.unsubscribe(ops.Document.signalCursorMoved, refreshLocalCaretBlinking);
223         odtDocument.unsubscribe(ops.Document.signalCursorRemoved, removeCaret);
224 
225         eventManager.unsubscribe("focus", focusLocalCaret);
226         eventManager.unsubscribe("blur", blurLocalCaret);
227         window.removeEventListener("focus", showLocalCaret, false);
228         window.removeEventListener("blur", hideLocalCaret, false);
229 
230         carets = {};
231         core.Async.destroyAll(caretCleanup, callback);
232     };
233 
234     function init() {
235         var odtDocument = sessionController.getSession().getOdtDocument(),
236             eventManager = sessionController.getEventManager();
237 
238         odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, ensureLocalCaretVisible);
239         odtDocument.subscribe(ops.Document.signalCursorMoved, refreshLocalCaretBlinking);
240         odtDocument.subscribe(ops.Document.signalCursorRemoved, removeCaret);
241 
242         eventManager.subscribe("focus", focusLocalCaret);
243         eventManager.subscribe("blur", blurLocalCaret);
244         window.addEventListener("focus", showLocalCaret, false);
245         window.addEventListener("blur", hideLocalCaret, false);
246     }
247 
248     init();
249 };
250