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