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