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, gui, odf, xmldom */ 26 27 /** 28 * @constructor 29 * @implements {core.Destroyable} 30 * @param {!function():!HTMLElement} getContainer Fetch the surrounding HTML container 31 * @param {!gui.KeyboardHandler} keyDownHandler 32 * @param {!gui.KeyboardHandler} keyUpHandler 33 */ 34 gui.HyperlinkClickHandler = function HyperlinkClickHandler(getContainer, keyDownHandler, keyUpHandler) { 35 "use strict"; 36 var /**@const 37 @type{!string}*/ 38 inactiveLinksCssClass = "webodf-inactiveLinks", 39 modifier = gui.KeyboardHandler.Modifier, 40 keyCode = gui.KeyboardHandler.KeyCode, 41 xpath = xmldom.XPath, 42 odfUtils = odf.OdfUtils, 43 window = /**@type{!Window}*/(runtime.getWindow()), 44 /**@type{!number}*/ 45 activeModifier = modifier.None, 46 /**@type{!Array.<!{keyCode: !number, modifier: !number}>}*/ 47 activeKeyBindings = []; 48 49 runtime.assert(window !== null, "Expected to be run in an environment which has a global window, like a browser."); 50 51 /** 52 * @param {?Node} node 53 * @return {?Element} 54 */ 55 function getHyperlinkElement(node) { 56 while (node !== null) { 57 if (odfUtils.isHyperlink(node)) { 58 return /**@type{!Element}*/(node); 59 } 60 if (odfUtils.isParagraph(node)) { 61 break; 62 } 63 node = node.parentNode; 64 } 65 return null; 66 } 67 68 /** 69 * @param {!Event} e 70 * @return {undefined} 71 */ 72 this.handleClick = function (e) { 73 var target = e.target || e.srcElement, 74 pressedModifier, 75 linkElement, 76 /**@type{!string}*/ 77 url, 78 rootNode, 79 bookmarks; 80 81 if (e.ctrlKey) { 82 pressedModifier = modifier.Ctrl; 83 } else if (e.metaKey) { 84 pressedModifier = modifier.Meta; 85 } 86 87 if (activeModifier !== modifier.None && activeModifier !== pressedModifier) { 88 return; 89 } 90 91 linkElement = getHyperlinkElement(/**@type{?Node}*/(target)); 92 if (!linkElement) { 93 return; 94 } 95 96 url = odfUtils.getHyperlinkTarget(linkElement); 97 if (url === "") { 98 return; 99 } 100 101 if (url[0] === '#') { // bookmark 102 url = url.substring(1); 103 rootNode = getContainer(); 104 bookmarks = xpath.getODFElementsWithXPath(rootNode, 105 "//text:bookmark-start[@text:name='" + url + "']", 106 odf.Namespaces.lookupNamespaceURI); 107 108 if (bookmarks.length === 0) { 109 bookmarks = xpath.getODFElementsWithXPath(rootNode, 110 "//text:bookmark[@text:name='" + url + "']", 111 odf.Namespaces.lookupNamespaceURI); 112 } 113 114 if (bookmarks.length > 0) { 115 bookmarks[0].scrollIntoView(true); 116 } 117 } else { 118 // Ask the browser to open the link in a new window. `javascript` and `data` URIs are disabled for 119 // security reasons. 120 if(/^\s*(javascript|data):/.test(url)) { 121 runtime.log("WARN:", "potentially malicious URL ignored"); 122 } else { 123 window.open(url); 124 } 125 } 126 127 if (e.preventDefault) { 128 e.preventDefault(); 129 } else { 130 e.returnValue = false; 131 } 132 }; 133 134 /** 135 * Show pointer cursor when hover over hyperlink 136 * @return {undefined} 137 */ 138 function showPointerCursor() { 139 var container = getContainer(); 140 runtime.assert(Boolean(container.classList), "Document container has no classList element"); 141 container.classList.remove(inactiveLinksCssClass); 142 } 143 144 /** 145 * Show text cursor when hover over hyperlink 146 * @return {undefined} 147 */ 148 function showTextCursor() { 149 var container = getContainer(); 150 runtime.assert(Boolean(container.classList), "Document container has no classList element"); 151 container.classList.add(inactiveLinksCssClass); 152 } 153 154 /** 155 * Remove all currently subscribed keyboard shortcuts & window events 156 * @return {undefined} 157 */ 158 function cleanupEventBindings() { 159 window.removeEventListener("focus", showTextCursor, false); 160 activeKeyBindings.forEach(function(boundShortcut) { 161 keyDownHandler.unbind(boundShortcut.keyCode, boundShortcut.modifier); 162 keyUpHandler.unbind(boundShortcut.keyCode, boundShortcut.modifier); 163 }); 164 activeKeyBindings.length = 0; 165 } 166 167 /** 168 * @param {!number} modifierKey 169 * @return {undefined} 170 */ 171 function bindEvents(modifierKey) { 172 cleanupEventBindings(); 173 174 if (modifierKey !== modifier.None) { 175 // Cursor style needs to be reset when the window loses focus otherwise the cursor hand will remain 176 // permanently on in some browsers due to the focus being switched and the keyup event never being received. 177 // eventManager binds to the focus event on both eventTrap and window, but we only specifically want 178 // the window focus event. 179 window.addEventListener("focus", showTextCursor, false); 180 181 switch (modifierKey) { 182 case modifier.Ctrl: 183 activeKeyBindings.push({keyCode: keyCode.Ctrl, modifier: modifier.None}); 184 break; 185 case modifier.Meta: 186 activeKeyBindings.push({keyCode: keyCode.LeftMeta, modifier: modifier.None}); 187 activeKeyBindings.push({keyCode: keyCode.RightMeta, modifier: modifier.None}); 188 activeKeyBindings.push({keyCode: keyCode.MetaInMozilla, modifier: modifier.None}); 189 break; 190 } 191 192 activeKeyBindings.forEach(function(boundShortcut) { 193 keyDownHandler.bind(boundShortcut.keyCode, boundShortcut.modifier, showPointerCursor); 194 keyUpHandler.bind(boundShortcut.keyCode, boundShortcut.modifier, showTextCursor); 195 }); 196 } 197 } 198 199 /** 200 * Sets the modifier key for activating the hyperlink. 201 * @param {!number} value 202 * @return {undefined} 203 */ 204 this.setModifier = function (value) { 205 if (activeModifier === value) { 206 return; 207 } 208 runtime.assert(value === modifier.None || value === modifier.Ctrl || value === modifier.Meta, 209 "Unsupported KeyboardHandler.Modifier value: " + value); 210 211 activeModifier = value; 212 if (activeModifier !== modifier.None) { 213 showTextCursor(); 214 } else { 215 showPointerCursor(); 216 } 217 bindEvents(activeModifier); 218 }; 219 220 /** 221 * Get the currently active modifier key. This will be equivalent to a value 222 * found in gui.KeyboardHandler.Modifier 223 * @return {!number} 224 */ 225 this.getModifier = function() { 226 return activeModifier; 227 }; 228 229 /** 230 * Destroy the object. 231 * Do not access any member of this object after this call. 232 * @param {function(!Error=):undefined} callback 233 * @return {undefined} 234 */ 235 this.destroy = function(callback) { 236 showTextCursor(); 237 cleanupEventBindings(); 238 callback(); 239 }; 240 }; 241