]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix some links not being targeted by `F`, `gf` and `gF`
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015.
4 # Copyright Wang Zhuochun 2013.
5 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 # This file contains lots of different helper functions.
23
24 HTMLAnchorElement = Ci.nsIDOMHTMLAnchorElement
25 HTMLButtonElement = Ci.nsIDOMHTMLButtonElement
26 HTMLInputElement = Ci.nsIDOMHTMLInputElement
27 HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
28 HTMLSelectElement = Ci.nsIDOMHTMLSelectElement
29 XULDocument = Ci.nsIDOMXULDocument
30 XULButtonElement = Ci.nsIDOMXULButtonElement
31 XULControlElement = Ci.nsIDOMXULControlElement
32 XULMenuListElement = Ci.nsIDOMXULMenuListElement
33 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
34
35 USE_CAPTURE = true
36
37
38
39 # Element classification helpers
40
41 isActivatable = (element) ->
42 return element instanceof HTMLAnchorElement or
43 element instanceof HTMLButtonElement or
44 (element instanceof HTMLInputElement and element.type in [
45 'button', 'submit', 'reset', 'image'
46 ]) or
47 element instanceof XULButtonElement
48
49 isAdjustable = (element) ->
50 return element instanceof HTMLInputElement and element.type in [
51 'checkbox', 'radio', 'file', 'color'
52 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
53 ] or
54 element instanceof XULControlElement or
55 # Youtube special case.
56 element.classList?.contains('html5-video-player') or
57 element.classList?.contains('ytp-button')
58
59 isContentEditable = (element) ->
60 return element.isContentEditable or
61 # `g_editable` is a non-standard attribute commonly used by Google.
62 element.getAttribute?('g_editable') == 'true' or
63 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
64
65 isProperLink = (element) ->
66 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
67 # href="">`s used as buttons on some sites.
68 return element.getAttribute('href') and
69 (element instanceof HTMLAnchorElement or
70 element.ownerDocument instanceof XULDocument) and
71 not element.href.endsWith('#') and
72 not element.href.endsWith('#?') and
73 not element.href.startsWith('javascript:')
74
75 isTextInputElement = (element) ->
76 return (element instanceof HTMLInputElement and element.type in [
77 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
78 ]) or
79 element instanceof HTMLTextAreaElement or
80 element instanceof XULTextBoxElement
81
82 isTypingElement = (element) ->
83 return isTextInputElement(element) or
84 # `<select>` elements can also receive text input: You may type the
85 # text of an item to select it.
86 element instanceof HTMLSelectElement or
87 element instanceof XULMenuListElement
88
89
90
91 # Active/focused element helpers
92
93 getActiveElement = (window) ->
94 {activeElement} = window.document
95 # If the active element is a frame, recurse into it. The easiest way to detect
96 # a frame that works both in browser UI and in web page content is to check
97 # for the presence of `.contentWindow`. However, in non-multi-process
98 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
99 # pointing to the web page content `window`, which we don’t want to recurse
100 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
101 # to be the only way to detect such elements.
102 if activeElement.localName != 'browser' and activeElement.contentWindow
103 return getActiveElement(activeElement.contentWindow)
104 else
105 return activeElement
106
107 blurActiveElement = (window) ->
108 return unless activeElement = getActiveElement(window)
109 activeElement.blur()
110
111 blurActiveBrowserElement = (vim) ->
112 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
113 # reset it, for example.
114 # - Focusing the current browser afterwards allows to pass `<escape>` as well
115 # as unbound keys to the page. However, focusing the browser also triggers
116 # focus events on `document` and `window` in the current page. Many pages
117 # re-focus some text input on those events, making it impossible to blur
118 # those! Therefore we tell the frame script to suppress those events.
119 {window} = vim
120 activeElement = getActiveElement(window)
121 vim._send('browserRefocus')
122 nextTick(window, ->
123 activeElement.blur()
124 window.gBrowser.selectedBrowser.focus()
125 )
126
127 # Focus an element and tell Firefox that the focus happened because of a user
128 # keypress (not just because some random programmatic focus).
129 focusElement = (element, options = {}) ->
130 focusManager = Cc['@mozilla.org/focus-manager;1']
131 .getService(Ci.nsIFocusManager)
132 focusManager.setFocus(element, focusManager.FLAG_BYKEY)
133 element.select?() if options.select
134
135 getFocusType = (element) -> switch
136 when isTypingElement(element) or isContentEditable(element)
137 'editable'
138 when isActivatable(element)
139 'activatable'
140 when isAdjustable(element)
141 'adjustable'
142 else
143 null
144
145
146
147 # Event helpers
148
149 listen = (element, eventName, listener, useCapture = true) ->
150 element.addEventListener(eventName, listener, useCapture)
151 module.onShutdown(->
152 element.removeEventListener(eventName, listener, useCapture)
153 )
154
155 listenOnce = (element, eventName, listener, useCapture = true) ->
156 fn = (event) ->
157 listener(event)
158 element.removeEventListener(eventName, fn, useCapture)
159 listen(element, eventName, fn, useCapture)
160
161 onRemoved = (window, element, fn) ->
162 mutationObserver = new window.MutationObserver((changes) ->
163 for change in changes then for removedElement in change.removedNodes
164 if removedElement == element
165 mutationObserver.disconnect()
166 fn()
167 return
168 )
169 mutationObserver.observe(element.parentNode, {childList: true})
170 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
171
172 suppressEvent = (event) ->
173 event.preventDefault()
174 event.stopPropagation()
175
176 # Simulate mouse click with a full chain of events. ('command' is for XUL
177 # elements.)
178 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
179 simulateClick = (element) ->
180 window = element.ownerGlobal
181 for type in eventSequence
182 mouseEvent = new window.MouseEvent(type, {
183 # Let the event bubble in order to trigger delegated event listeners.
184 bubbles: true
185 # Make the event cancelable so that `<a href="#">` can be used as a
186 # JavaScript-powered button without scrolling to the top of the page.
187 cancelable: true
188 })
189 element.dispatchEvent(mouseEvent)
190 return
191
192
193
194 # DOM helpers
195
196 area = (element) ->
197 return element.clientWidth * element.clientHeight
198
199 containsDeep = (parent, element) ->
200 parentWindow = parent.ownerGlobal
201 elementWindow = element.ownerGlobal
202
203 while elementWindow != parentWindow and elementWindow.top != elementWindow
204 element = elementWindow.frameElement
205 elementWindow = element.ownerGlobal
206
207 return parent.contains(element)
208
209 createBox = (document, className = '', parent = null, text = null) ->
210 box = document.createElement('box')
211 box.className = "#{className} vimfx-box"
212 box.textContent = text if text?
213 parent.appendChild(box) if parent?
214 return box
215
216 insertText = (input, value) ->
217 {selectionStart, selectionEnd} = input
218 input.value =
219 input.value[0...selectionStart] + value + input.value[selectionEnd..]
220 input.selectionStart = input.selectionEnd = selectionStart + value.length
221
222 querySelectorAllDeep = (window, selector) ->
223 elements = Array.from(window.document.querySelectorAll(selector))
224 for frame in window.frames
225 elements.push(querySelectorAllDeep(frame, selector)...)
226 return elements
227
228 setAttributes = (element, attributes) ->
229 for attribute, value of attributes
230 element.setAttribute(attribute, value)
231 return
232
233
234
235 # Language helpers
236
237 class Counter
238 constructor: ({start: @value = 0, @step = 1}) ->
239 tick: -> @value += @step
240
241 class EventEmitter
242 constructor: ->
243 @listeners = {}
244
245 on: (event, listener) ->
246 (@listeners[event] ?= []).push(listener)
247
248 emit: (event, data) ->
249 for listener in @listeners[event] ? []
250 listener(data)
251 return
252
253 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
254
255 nextTick = (window, fn) -> window.setTimeout(fn, 0)
256
257 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
258
259 removeDuplicates = (array) -> Array.from(new Set(array))
260
261 # Remove duplicate characters from string (case insensitive).
262 removeDuplicateCharacters = (str) ->
263 return removeDuplicates( str.toLowerCase().split('') ).join('')
264
265
266
267 # Misc helpers
268
269 formatError = (error) ->
270 stack = String(error.stack?.formattedStack ? error.stack ? '')
271 .split('\n')
272 .filter((line) -> line.includes('.xpi!'))
273 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
274 .join('\n')
275 return "#{error}\n#{stack}"
276
277 getCurrentLocation = ->
278 window = getCurrentWindow()
279 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
280
281 getCurrentWindow = ->
282 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
283 .getService(Components.interfaces.nsIWindowMediator)
284 return windowMediator.getMostRecentWindow('navigator:browser')
285
286 loadCss = (name) ->
287 sss = Cc['@mozilla.org/content/style-sheet-service;1']
288 .getService(Ci.nsIStyleSheetService)
289 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
290 method = sss.AUTHOR_SHEET
291 unless sss.sheetRegistered(uri, method)
292 sss.loadAndRegisterSheet(uri, method)
293 module.onShutdown(->
294 sss.unregisterSheet(uri, method)
295 )
296
297 observe = (topic, observer) ->
298 observer = {observe: observer} if typeof observer == 'function'
299 Services.obs.addObserver(observer, topic, false)
300 module.onShutdown(->
301 Services.obs.removeObserver(observer, topic, false)
302 )
303
304 openTab = (window, url, options) ->
305 {gBrowser} = window
306 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
307 gBrowser.loadOneTab(url, options)
308
309 writeToClipboard = (text) ->
310 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
311 .getService(Ci.nsIClipboardHelper)
312 clipboardHelper.copyString(text)
313
314
315
316 module.exports = {
317 isActivatable
318 isAdjustable
319 isContentEditable
320 isProperLink
321 isTextInputElement
322 isTypingElement
323
324 getActiveElement
325 blurActiveElement
326 blurActiveBrowserElement
327 focusElement
328 getFocusType
329
330 listen
331 listenOnce
332 onRemoved
333 suppressEvent
334 simulateClick
335
336 area
337 containsDeep
338 createBox
339 insertText
340 querySelectorAllDeep
341 setAttributes
342
343 Counter
344 EventEmitter
345 has
346 nextTick
347 regexEscape
348 removeDuplicates
349 removeDuplicateCharacters
350
351 formatError
352 getCurrentLocation
353 getCurrentWindow
354 loadCss
355 observe
356 openTab
357 writeToClipboard
358 }
Imprint / Impressum