1 /**
  2  * Copyright (C) 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, odf, ops, Node*/
 26 
 27 /**
 28  * @constructor
 29  * @implements {core.Destroyable}
 30  * @param {!ops.Session} session
 31  * @param {!gui.SessionConstraints} sessionConstraints
 32  * @param {!string} inputMemberId
 33  */
 34 gui.AnnotationController = function AnnotationController(session, sessionConstraints, inputMemberId) {
 35     "use strict";
 36 
 37     var odtDocument = session.getOdtDocument(),
 38         isAnnotatable = false,
 39         eventNotifier = new core.EventNotifier([gui.AnnotationController.annotatableChanged]),
 40         odfUtils = odf.OdfUtils,
 41         /**@const*/
 42         NEXT = core.StepDirection.NEXT;
 43 
 44     /**
 45      * @return {undefined}
 46      */
 47     function updatedCachedValues() {
 48         var cursor = odtDocument.getCursor(inputMemberId),
 49             cursorNode = cursor && cursor.getNode(),
 50             newIsAnnotatable = false;
 51         if (cursorNode) {
 52             newIsAnnotatable = !odfUtils.isWithinAnnotation(cursorNode, odtDocument.getRootNode());
 53         }
 54 
 55         if (newIsAnnotatable !== isAnnotatable) {
 56             isAnnotatable = newIsAnnotatable;
 57             eventNotifier.emit(gui.AnnotationController.annotatableChanged, isAnnotatable);
 58         }
 59     }
 60 
 61     /**
 62      * @param {!ops.OdtCursor} cursor
 63      * @return {undefined}
 64      */
 65     function onCursorAdded(cursor) {
 66         if (cursor.getMemberId() === inputMemberId) {
 67             updatedCachedValues();
 68         }
 69     }
 70 
 71     /**
 72      * @param {!string} memberId
 73      * @return {undefined}
 74      */
 75     function onCursorRemoved(memberId) {
 76         if (memberId === inputMemberId) {
 77             updatedCachedValues();
 78         }
 79     }
 80 
 81     /**
 82      * @param {!ops.OdtCursor} cursor
 83      * @return {undefined}
 84      */
 85     function onCursorMoved(cursor) {
 86         if (cursor.getMemberId() === inputMemberId) {
 87             updatedCachedValues();
 88         }
 89     }
 90 
 91     /**
 92      * @return {!boolean}
 93      */
 94     this.isAnnotatable = function () {
 95         return isAnnotatable;
 96     };
 97 
 98     /**
 99      * Adds an annotation to the document based on the current selection
100      * @return {undefined}
101      */
102     this.addAnnotation = function () {
103         var op = new ops.OpAddAnnotation(),
104             selection = odtDocument.getCursorSelection(inputMemberId),
105             length = selection.length,
106             position = selection.position;
107 
108         if (!isAnnotatable) {
109             return;
110         }
111 
112         if (length === 0) {
113             length = undefined;
114         } else {
115             position = length >= 0 ? position : position + length;
116             length = Math.abs(length);
117         }
118 
119         op.init({
120             memberid: inputMemberId,
121             position: position,
122             length: length,
123             name: inputMemberId + Date.now()
124         });
125         session.enqueue([op]);
126     };
127 
128 
129     /**
130      * @param {!Element} annotationNode
131      * @return {undefined}
132      */
133     this.removeAnnotation = function(annotationNode) {
134         var startStep, endStep, op, moveCursor,
135             currentUserName = odtDocument.getMember(inputMemberId).getProperties().fullName;
136 
137         if (sessionConstraints.getState(gui.CommonConstraints.EDIT.ANNOTATIONS.ONLY_DELETE_OWN) === true) {
138             if (currentUserName !== odfUtils.getAnnotationCreator(annotationNode)) {
139                 return;
140             }
141         }
142 
143         // round up to get the first step within the annotation node
144         startStep = odtDocument.convertDomPointToCursorStep(annotationNode, 0, NEXT);
145         // Will report the last walkable step within the annotation
146         endStep = odtDocument.convertDomPointToCursorStep(annotationNode, annotationNode.childNodes.length);
147 
148         op = new ops.OpRemoveAnnotation();
149         op.init({
150             memberid: inputMemberId,
151             position: startStep,
152             length: endStep - startStep
153         });
154         moveCursor = new ops.OpMoveCursor();
155         moveCursor.init({
156             memberid: inputMemberId,
157             position: startStep > 0 ? startStep - 1 : startStep, // Last position just before the annotation starts
158             length: 0
159         });
160         session.enqueue([op, moveCursor]);
161     };
162 
163     /**
164      * @param {!string} eventid
165      * @param {!Function} cb
166      * @return {undefined}
167      */
168     this.subscribe = function (eventid, cb) {
169         eventNotifier.subscribe(eventid, cb);
170     };
171 
172     /**
173      * @param {!string} eventid
174      * @param {!Function} cb
175      * @return {undefined}
176      */
177     this.unsubscribe = function (eventid, cb) {
178         eventNotifier.unsubscribe(eventid, cb);
179     };
180 
181     /**
182      * @param {!function(!Error=)} callback, passing an error object in case of error
183      * @return {undefined}
184      */
185     this.destroy = function(callback) {
186         odtDocument.unsubscribe(ops.Document.signalCursorAdded, onCursorAdded);
187         odtDocument.unsubscribe(ops.Document.signalCursorRemoved, onCursorRemoved);
188         odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorMoved);
189         callback();
190     };
191 
192     function init() {
193         sessionConstraints.registerConstraint(gui.CommonConstraints.EDIT.ANNOTATIONS.ONLY_DELETE_OWN);
194 
195         odtDocument.subscribe(ops.Document.signalCursorAdded, onCursorAdded);
196         odtDocument.subscribe(ops.Document.signalCursorRemoved, onCursorRemoved);
197         odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorMoved);
198         updatedCachedValues();
199     }
200 
201     init();
202 };
203 
204 /**@const*/gui.AnnotationController.annotatableChanged = "annotatable/changed";
205