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. 119 window.open(url); 120 } 121 122 if (e.preventDefault) { 123 e.preventDefault(); 124 } else { 125 e.returnValue = false; 126 } 127 }; 128 129 /** 130 * Show pointer cursor when hover over hyperlink 131 * @return {undefined} 132 */ 133 function showPointerCursor() { 134 var container = getContainer(); 135 runtime.assert(Boolean(container.classList), "Document container has no classList element"); 136 container.classList.remove(inactiveLinksCssClass); 137 } 138 139 /** 140 * Show text cursor when hover over hyperlink 141 * @return {undefined} 142 */ 143 function showTextCursor() { 144 var container = getContainer(); 145 runtime.assert(Boolean(container.classList), "Document container has no classList element"); 146 container.classList.add(inactiveLinksCssClass); 147 } 148 149 /** 150 * Remove all currently subscribed keyboard shortcuts & window events 151 * @return {undefined} 152 */ 153 function cleanupEventBindings() { 154 window.removeEventListener("focus", showTextCursor, false); 155 activeKeyBindings.forEach(function(boundShortcut) { 156 keyDownHandler.unbind(boundShortcut.keyCode, boundShortcut.modifier); 157 keyUpHandler.unbind(boundShortcut.keyCode, boundShortcut.modifier); 158 }); 159 activeKeyBindings.length = 0; 160 } 161 162 /** 163 * @param {!number} modifierKey 164 * @return {undefined} 165 */ 166 function bindEvents(modifierKey) { 167 cleanupEventBindings(); 168 169 if (modifierKey !== modifier.None) { 170 // Cursor style needs to be reset when the window loses focus otherwise the cursor hand will remain 171 // permanently on in some browsers due to the focus being switched and the keyup event never being received. 172 // eventManager binds to the focus event on both eventTrap and window, but we only specifically want 173 // the window focus event. 174 window.addEventListener("focus", showTextCursor, false); 175 176 switch (modifierKey) { 177 case modifier.Ctrl: 178 activeKeyBindings.push({keyCode: keyCode.Ctrl, modifier: modifier.None}); 179 break; 180 case modifier.Meta: 181 activeKeyBindings.push({keyCode: keyCode.LeftMeta, modifier: modifier.None}); 182 activeKeyBindings.push({keyCode: keyCode.RightMeta, modifier: modifier.None}); 183 activeKeyBindings.push({keyCode: keyCode.MetaInMozilla, modifier: modifier.None}); 184 break; 185 } 186 187 activeKeyBindings.forEach(function(boundShortcut) { 188 keyDownHandler.bind(boundShortcut.keyCode, boundShortcut.modifier, showPointerCursor); 189 keyUpHandler.bind(boundShortcut.keyCode, boundShortcut.modifier, showTextCursor); 190 }); 191 } 192 } 193 194 /** 195 * Sets the modifier key for activating the hyperlink. 196 * @param {!number} value 197 * @return {undefined} 198 */ 199 this.setModifier = function (value) { 200 if (activeModifier === value) { 201 return; 202 } 203 runtime.assert(value === modifier.None || value === modifier.Ctrl || value === modifier.Meta, 204 "Unsupported KeyboardHandler.Modifier value: " + value); 205 206 activeModifier = value; 207 if (activeModifier !== modifier.None) { 208 showTextCursor(); 209 } else { 210 showPointerCursor(); 211 } 212 bindEvents(activeModifier); 213 }; 214 215 /** 216 * Get the currently active modifier key. This will be equivalent to a value 217 * found in gui.KeyboardHandler.Modifier 218 * @return {!number} 219 */ 220 this.getModifier = function() { 221 return activeModifier; 222 }; 223 224 /** 225 * Destroy the object. 226 * Do not access any member of this object after this call. 227 * @param {function(!Error=):undefined} callback 228 * @return {undefined} 229 */ 230 this.destroy = function(callback) { 231 showTextCursor(); 232 cleanupEventBindings(); 233 callback(); 234 }; 235 }; 236