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