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 = new 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