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