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 * @param {!gui.Viewport} viewport 38 */ 39 gui.CaretManager = function CaretManager(sessionController, viewport) { 40 "use strict"; 41 var /**@type{!Object.<string,!gui.Caret>}*/ 42 carets = {}, 43 window = runtime.getWindow(), 44 odtDocument = sessionController.getSession().getOdtDocument(), 45 eventManager = sessionController.getEventManager(); 46 47 /** 48 * @param {!string} memberId 49 * @return {?gui.Caret} 50 */ 51 function getCaret(memberId) { 52 return carets.hasOwnProperty(memberId) ? carets[memberId] : null; 53 } 54 55 /** 56 * Get the horizontal offset of the local caret from the 57 * left edge of the screen (in pixels). 58 * @return {!number|undefined} 59 */ 60 function getLocalCaretXOffsetPx() { 61 var localCaret = getCaret(sessionController.getInputMemberId()), 62 lastRect; 63 if (localCaret) { 64 lastRect = localCaret.getBoundingClientRect(); 65 } 66 // usually the rect is 1px width, so rect.left ~= rect.right. 67 // Right is used because during IME composition the caret width includes 68 // the chars being composed. The caret is *always* flush against the right side 69 // of the it's BCR. 70 return lastRect ? lastRect.right : undefined; 71 } 72 73 /** 74 * @return {!Array.<!gui.Caret>} 75 */ 76 function getCarets() { 77 return Object.keys(carets).map(function (memberid) { 78 return carets[memberid]; 79 }); 80 } 81 82 /** 83 * @param {!string} memberId 84 * @return {undefined} 85 */ 86 function removeCaret(memberId) { 87 var caret = carets[memberId]; 88 if (caret) { 89 // Remove the caret before destroying it in case the destroy function causes new window/webodf events to be 90 // triggered. This ensures the caret can't receive any new events once destroy has been invoked 91 delete carets[memberId]; 92 if (memberId === sessionController.getInputMemberId()) { 93 odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.ensureVisible); 94 odtDocument.unsubscribe(ops.Document.signalCursorMoved, caret.refreshCursorBlinking); 95 96 eventManager.unsubscribe("compositionupdate", caret.handleUpdate); 97 eventManager.unsubscribe("compositionend", caret.handleUpdate); 98 eventManager.unsubscribe("focus", caret.setFocus); 99 eventManager.unsubscribe("blur", caret.removeFocus); 100 101 window.removeEventListener("focus", caret.show, false); 102 window.removeEventListener("blur", caret.hide, false); 103 } else { 104 odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.handleUpdate); 105 } 106 /*jslint emptyblock:true*/ 107 caret.destroy(function() {}); 108 /*jslint emptyblock:false*/ 109 } 110 } 111 112 /** 113 * @param {!ops.OdtCursor} cursor 114 * @param {!boolean} caretAvatarInitiallyVisible Set to false to hide the associated avatar 115 * @param {!boolean} blinkOnRangeSelect Specify that the caret should blink if a non-collapsed range is selected 116 * @return {!gui.Caret} 117 */ 118 this.registerCursor = function (cursor, caretAvatarInitiallyVisible, blinkOnRangeSelect) { 119 var memberid = cursor.getMemberId(), 120 caret = new gui.Caret(cursor, viewport, caretAvatarInitiallyVisible, blinkOnRangeSelect); 121 122 carets[memberid] = caret; 123 124 // if local input member, then let controller listen on caret span 125 if (memberid === sessionController.getInputMemberId()) { 126 runtime.log("Starting to track input on new cursor of " + memberid); 127 odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.ensureVisible); 128 odtDocument.subscribe(ops.Document.signalCursorMoved, caret.refreshCursorBlinking); 129 130 eventManager.subscribe("compositionupdate", caret.handleUpdate); 131 eventManager.subscribe("compositionend", caret.handleUpdate); 132 eventManager.subscribe("focus", caret.setFocus); 133 eventManager.subscribe("blur", caret.removeFocus); 134 135 window.addEventListener("focus", caret.show, false); 136 window.addEventListener("blur", caret.hide, false); 137 138 // Add event trap as an overlay element to the caret 139 caret.setOverlayElement(eventManager.getEventTrap()); 140 } else { 141 odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.handleUpdate); 142 } 143 144 return caret; 145 }; 146 147 /** 148 * @param {!string} memberId 149 * @return {?gui.Caret} 150 */ 151 this.getCaret = getCaret; 152 153 /** 154 * @return {!Array.<!gui.Caret>} 155 */ 156 this.getCarets = getCarets; 157 158 /** 159 * @param {!function(!Error=)} callback, passing an error object in case of error 160 * @return {undefined} 161 */ 162 this.destroy = function (callback) { 163 var caretCleanup = getCarets().map(function(caret) { return caret.destroy; }); 164 165 sessionController.getSelectionController().setCaretXPositionLocator(null); 166 odtDocument.unsubscribe(ops.Document.signalCursorRemoved, removeCaret); 167 carets = {}; 168 core.Async.destroyAll(caretCleanup, callback); 169 }; 170 171 function init() { 172 sessionController.getSelectionController().setCaretXPositionLocator(getLocalCaretXOffsetPx); 173 odtDocument.subscribe(ops.Document.signalCursorRemoved, removeCaret); 174 } 175 176 init(); 177 }; 178