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