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, Node, ops, odf */
 26 
 27 /**
 28  * @constructor
 29  * @implements {core.Destroyable}
 30  * @implements {core.EventSource}
 31  * @param {!ops.Session} session
 32  * @param {!gui.SessionConstraints} sessionConstraints
 33  * @param {!gui.SessionContext} sessionContext
 34  * @param {!string} inputMemberId
 35  */
 36 gui.HyperlinkController = function HyperlinkController(
 37     session,
 38     sessionConstraints,
 39     sessionContext,
 40     inputMemberId
 41     ) {
 42     "use strict";
 43 
 44     var odfUtils = odf.OdfUtils,
 45         odtDocument = session.getOdtDocument(),
 46         eventNotifier = new core.EventNotifier([
 47             gui.HyperlinkController.enabledChanged
 48         ]),
 49         isEnabled = false;
 50 
 51     /**
 52      * @return {undefined}
 53      */
 54     function updateEnabledState() {
 55         var /**@type{!boolean}*/newIsEnabled = true;
 56 
 57         if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) {
 58             newIsEnabled = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation());
 59         }
 60 
 61         if (newIsEnabled !== isEnabled) {
 62             isEnabled = newIsEnabled;
 63             eventNotifier.emit(gui.HyperlinkController.enabledChanged, isEnabled);
 64         }
 65     }
 66 
 67     /**
 68      * @param {!ops.OdtCursor} cursor
 69      * @return {undefined}
 70      */
 71     function onCursorEvent(cursor) {
 72         if (cursor.getMemberId() === inputMemberId) {
 73             updateEnabledState();
 74         }
 75     }
 76 
 77     /**
 78      * @return {!boolean}
 79      */
 80     this.isEnabled = function () {
 81         return isEnabled;
 82     };
 83 
 84     /**
 85      * @param {!string} eventid
 86      * @param {!Function} cb
 87      * @return {undefined}
 88      */
 89     this.subscribe = function (eventid, cb) {
 90         eventNotifier.subscribe(eventid, cb);
 91     };
 92 
 93     /**
 94      * @param {!string} eventid
 95      * @param {!Function} cb
 96      * @return {undefined}
 97      */
 98     this.unsubscribe = function (eventid, cb) {
 99         eventNotifier.unsubscribe(eventid, cb);
100     };
101 
102     /**
103      * Convert the current selection into a hyperlink
104      * @param {!string} hyperlink Hyperlink to insert
105      * @param {!string=} insertionText Optional text to insert as the text content for the hyperlink.
106      *  Note, the insertion text will not replace the existing selection content.
107      */
108     function addHyperlink(hyperlink, insertionText) {
109         if (!isEnabled) {
110             return;
111         }
112         var selection = odtDocument.getCursorSelection(inputMemberId),
113             op = new ops.OpApplyHyperlink(),
114             operations = [];
115 
116         if (selection.length === 0 || insertionText) {
117             insertionText = insertionText || hyperlink;
118             op = new ops.OpInsertText();
119             op.init({
120                 memberid: inputMemberId,
121                 position: selection.position,
122                 text: insertionText
123             });
124             selection.length = insertionText.length;
125             operations.push(op);
126         }
127 
128         op = new ops.OpApplyHyperlink();
129         op.init({
130             memberid: inputMemberId,
131             position: selection.position,
132             length: selection.length,
133             hyperlink: hyperlink
134         });
135         operations.push(op);
136         session.enqueue(operations);
137     }
138     this.addHyperlink = addHyperlink;
139 
140     /**
141      * Remove all hyperlinks within the current selection. If a range of text is selected,
142      * this will only unlink the selection. If the current selection is collapsed within a
143      * link, that entire link will be removed.
144      */
145     function removeHyperlinks() {
146         if (!isEnabled) {
147             return;
148         }
149         
150         var iterator = odtDocument.createPositionIterator(odtDocument.getRootNode()),
151             selectedRange = odtDocument.getCursor(inputMemberId).getSelectedRange(),
152             links = odfUtils.getHyperlinkElements(selectedRange),
153             removeEntireLink = selectedRange.collapsed && links.length === 1,
154             domRange = odtDocument.getDOMDocument().createRange(),
155             operations = [],
156             /**@type{{position: !number, length: number}}*/
157             cursorRange,
158             firstLink, lastLink, offset, op;
159 
160         if (links.length === 0) {
161             return;
162         }
163 
164         // Remove any links that overlap with the current selection
165         links.forEach(function (link) {
166             domRange.selectNodeContents(link);
167             cursorRange = odtDocument.convertDomToCursorRange({
168                 anchorNode: /**@type{!Node}*/(domRange.startContainer),
169                 anchorOffset: domRange.startOffset,
170                 focusNode: /**@type{!Node}*/(domRange.endContainer),
171                 focusOffset: domRange.endOffset
172             });
173             op = new ops.OpRemoveHyperlink();
174             op.init({
175                 memberid: inputMemberId,
176                 position: cursorRange.position,
177                 length: cursorRange.length
178             });
179             operations.push(op);
180         });
181 
182         if (!removeEntireLink) {
183             // Re-add any leading or trailing links that were only partially selected
184             firstLink = /**@type{!Element}*/(links[0]);
185             if (selectedRange.comparePoint(firstLink, 0) === -1) {
186                 domRange.setStart(firstLink, 0);
187                 domRange.setEnd(selectedRange.startContainer, selectedRange.startOffset);
188                 cursorRange = odtDocument.convertDomToCursorRange({
189                     anchorNode: /**@type{!Node}*/(domRange.startContainer),
190                     anchorOffset: domRange.startOffset,
191                     focusNode: /**@type{!Node}*/(domRange.endContainer),
192                     focusOffset: domRange.endOffset
193                 });
194                 if (cursorRange.length > 0) {
195                     op = new ops.OpApplyHyperlink();
196                     /**@type{!ops.OpApplyHyperlink}*/(op).init({
197                         memberid: inputMemberId,
198                         position: cursorRange.position,
199                         length: cursorRange.length,
200                         hyperlink: odfUtils.getHyperlinkTarget(firstLink)
201                     });
202                     operations.push(op);
203                 }
204             }
205             lastLink = /**@type{!Element}*/(links[links.length - 1]);
206             iterator.moveToEndOfNode(lastLink);
207             offset = iterator.unfilteredDomOffset();
208             if (selectedRange.comparePoint(lastLink, offset) === 1) {
209                 domRange.setStart(selectedRange.endContainer, selectedRange.endOffset);
210                 domRange.setEnd(lastLink, offset);
211                 cursorRange = odtDocument.convertDomToCursorRange({
212                     anchorNode: /**@type{!Node}*/(domRange.startContainer),
213                     anchorOffset: domRange.startOffset,
214                     focusNode: /**@type{!Node}*/(domRange.endContainer),
215                     focusOffset: domRange.endOffset
216                 });
217                 if (cursorRange.length > 0) {
218                     op = new ops.OpApplyHyperlink();
219                     /**@type{!ops.OpApplyHyperlink}*/(op).init({
220                         memberid: inputMemberId,
221                         position: cursorRange.position,
222                         length: cursorRange.length,
223                         hyperlink: odfUtils.getHyperlinkTarget(lastLink)
224                     });
225                     operations.push(op);
226                 }
227             }
228         }
229 
230         session.enqueue(operations);
231         domRange.detach();
232     }
233     this.removeHyperlinks = removeHyperlinks;
234 
235     /**
236      * @param {!function(!Error=)} callback, passing an error object in case of error
237      * @return {undefined}
238      */
239     this.destroy = function (callback) {
240         odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent);
241         sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
242         callback();
243     };
244 
245     function init() {
246         odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent);
247         sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
248         updateEnabledState();
249     }
250     init();
251 };
252 
253 /**@const*/gui.HyperlinkController.enabledChanged = "enabled/changed";
254