]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix uncaught exception when loading some pages
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
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 HTMLBodyElement = Ci.nsIDOMHTMLBodyElement
30 XULDocument = Ci.nsIDOMXULDocument
31 XULButtonElement = Ci.nsIDOMXULButtonElement
32 XULControlElement = Ci.nsIDOMXULControlElement
33 XULMenuListElement = Ci.nsIDOMXULMenuListElement
34 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
35
36 # Full chains of events for different mouse actions. ('command' is for XUL
37 # elements.)
38 EVENTS_CLICK = ['mousedown', 'mouseup', 'click', 'command']
39 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
40 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
41
42
43
44 # Element classification helpers
45
46 isActivatable = (element) ->
47 return element instanceof HTMLAnchorElement or
48 element instanceof HTMLButtonElement or
49 (element instanceof HTMLInputElement and element.type in [
50 'button', 'submit', 'reset', 'image'
51 ]) or
52 element instanceof XULButtonElement
53
54 isAdjustable = (element) ->
55 return element instanceof HTMLInputElement and element.type in [
56 'checkbox', 'radio', 'file', 'color'
57 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
58 ] or
59 element instanceof XULControlElement or
60 # Youtube special case.
61 element.classList?.contains('html5-video-player') or
62 element.classList?.contains('ytp-button')
63
64 isContentEditable = (element) ->
65 return element.isContentEditable or
66 isIframeEditor(element) or
67 # Google.
68 element.getAttribute?('g_editable') == 'true' or
69 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
70
71 isIframeEditor = (element) ->
72 return false unless element instanceof HTMLBodyElement
73 return \
74 # Etherpad.
75 element.id == 'innerdocbody' or
76 # XpressEditor.
77 (element.classList.contains('xe_content') and
78 element.classList.contains('editable')) or
79 # vBulletin.
80 element.classList.contains('wysiwyg')
81
82 isProperLink = (element) ->
83 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
84 # href="">`s used as buttons on some sites.
85 return element.getAttribute('href') and
86 (element instanceof HTMLAnchorElement or
87 element.ownerDocument instanceof XULDocument) and
88 not element.href.endsWith('#') and
89 not element.href.endsWith('#?') and
90 not element.href.startsWith('javascript:')
91
92 isTextInputElement = (element) ->
93 return (element instanceof HTMLInputElement and element.type in [
94 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
95 ]) or
96 element instanceof HTMLTextAreaElement or
97 element instanceof XULTextBoxElement or
98 isContentEditable(element)
99
100 isTypingElement = (element) ->
101 return isTextInputElement(element) or
102 # `<select>` elements can also receive text input: You may type the
103 # text of an item to select it.
104 element instanceof HTMLSelectElement or
105 element instanceof XULMenuListElement
106
107
108
109 # Active/focused element helpers
110
111 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
112 # is loading. Therefore always check if anything was returned, such as:
113 #
114 # return unless activeElement = utils.getActiveElement(window)
115 getActiveElement = (window) ->
116 {activeElement} = window.document
117 return null unless activeElement
118 # If the active element is a frame, recurse into it. The easiest way to detect
119 # a frame that works both in browser UI and in web page content is to check
120 # for the presence of `.contentWindow`. However, in non-multi-process
121 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
122 # pointing to the web page content `window`, which we don’t want to recurse
123 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
124 # to be the only way to detect such elements.
125 if activeElement.localName != 'browser' and activeElement.contentWindow
126 return getActiveElement(activeElement.contentWindow)
127 else
128 return activeElement
129
130 blurActiveElement = (window) ->
131 # Blurring a frame element also blurs any active elements inside it. Recursing
132 # into the frames and blurring the “real” active element directly would give
133 # focus to the `<body>` of its containing frame, while blurring the top-most
134 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
135 # editors which use an `<iframe>` as their text area.
136 window.document.activeElement?.blur()
137
138 blurActiveBrowserElement = (vim) ->
139 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
140 # reset it, for example.
141 # - Focusing the current browser afterwards allows to pass `<escape>` as well
142 # as unbound keys to the page. However, focusing the browser also triggers
143 # focus events on `document` and `window` in the current page. Many pages
144 # re-focus some text input on those events, making it impossible to blur
145 # those! Therefore we tell the frame script to suppress those events.
146 {window} = vim
147 activeElement = getActiveElement(window)
148 vim._send('browserRefocus')
149 nextTick(window, ->
150 activeElement.blur()
151 window.gBrowser.selectedBrowser.focus()
152 )
153
154 # Focus an element and tell Firefox that the focus happened because of a user
155 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
156 # look more appropriate, but it unconditionally selects all text, which
157 # `.FLAG_BYMOUSE` does not.
158 focusElement = (element, options = {}) ->
159 focusManager = Cc['@mozilla.org/focus-manager;1']
160 .getService(Ci.nsIFocusManager)
161 focusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
162 element.select?() if options.select
163
164 getFocusType = (element) -> switch
165 when isTypingElement(element)
166 'editable'
167 when isActivatable(element)
168 'activatable'
169 when isAdjustable(element)
170 'adjustable'
171 else
172 null
173
174
175
176 # Event helpers
177
178 listen = (element, eventName, listener, useCapture = true) ->
179 element.addEventListener(eventName, listener, useCapture)
180 module.onShutdown(->
181 element.removeEventListener(eventName, listener, useCapture)
182 )
183
184 listenOnce = (element, eventName, listener, useCapture = true) ->
185 fn = (event) ->
186 listener(event)
187 element.removeEventListener(eventName, fn, useCapture)
188 listen(element, eventName, fn, useCapture)
189
190 onRemoved = (window, element, fn) ->
191 mutationObserver = new window.MutationObserver((changes) ->
192 for change in changes then for removedElement in change.removedNodes
193 if removedElement == element
194 mutationObserver.disconnect()
195 fn()
196 return
197 )
198 mutationObserver.observe(element.parentNode, {childList: true})
199 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
200
201 suppressEvent = (event) ->
202 event.preventDefault()
203 event.stopPropagation()
204
205 simulateMouseEvents = (element, sequenceType) ->
206 window = element.ownerGlobal
207 rect = element.getBoundingClientRect()
208
209 eventSequence = switch sequenceType
210 when 'click' then EVENTS_CLICK
211 when 'hover-start' then EVENTS_HOVER_START
212 when 'hover-end' then EVENTS_HOVER_END
213
214 for type in eventSequence
215 mouseEvent = new window.MouseEvent(type, {
216 # Let the event bubble in order to trigger delegated event listeners.
217 bubbles: type not in ['mouseenter', 'mouseleave']
218 # Make the event cancelable so that `<a href="#">` can be used as a
219 # JavaScript-powered button without scrolling to the top of the page.
220 cancelable: type not in ['mouseenter', 'mouseleave']
221 # These properties are just here for mimicing a real click as much as
222 # possible.
223 buttons: if type in EVENTS_CLICK then 1 else 0
224 detail: if type in EVENTS_CLICK then 1 else 0
225 view: window
226 # `page{X,Y}` are set automatically to the correct values when setting
227 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
228 # to set.
229 clientX: rect.left
230 clientY: rect.top
231 # To exactly calculate `screen{X,Y}` one has to to check where the web
232 # page content area is inside the browser chrome and go through all parent
233 # frames as well. This is good enough. YAGNI for now.
234 screenX: window.screenX + rect.left
235 screenY: window.screenY + rect.top
236 })
237 element.dispatchEvent(mouseEvent)
238
239 return
240
241
242
243 # DOM helpers
244
245 area = (element) ->
246 return element.clientWidth * element.clientHeight
247
248 containsDeep = (parent, element) ->
249 parentWindow = parent.ownerGlobal
250 elementWindow = element.ownerGlobal
251
252 while elementWindow != parentWindow and elementWindow.top != elementWindow
253 element = elementWindow.frameElement
254 elementWindow = element.ownerGlobal
255
256 return parent.contains(element)
257
258 createBox = (document, className = '', parent = null, text = null) ->
259 box = document.createElement('box')
260 box.className = "#{className} vimfx-box"
261 box.textContent = text if text?
262 parent.appendChild(box) if parent?
263 return box
264
265 injectTemporaryPopup = (document, contents) ->
266 popup = document.createElement('menupopup')
267 popup.appendChild(contents)
268 document.getElementById('mainPopupSet').appendChild(popup)
269 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
270 return popup
271
272 insertText = (input, value) ->
273 {selectionStart, selectionEnd} = input
274 input.value =
275 input.value[0...selectionStart] + value + input.value[selectionEnd..]
276 input.selectionStart = input.selectionEnd = selectionStart + value.length
277
278 querySelectorAllDeep = (window, selector) ->
279 elements = Array.from(window.document.querySelectorAll(selector))
280 for frame in window.frames
281 elements.push(querySelectorAllDeep(frame, selector)...)
282 return elements
283
284 scroll = (element, args) ->
285 {method, type, directions, amounts, properties, adjustment, smooth} = args
286 options = {}
287 for direction, index in directions
288 amount = amounts[index]
289 options[direction] = -Math.sign(amount) * adjustment + switch type
290 when 'lines' then amount
291 when 'pages' then amount * element[properties[index]]
292 when 'other' then Math.min(amount, element[properties[index]])
293 options.behavior = 'smooth' if smooth
294 element[method](options)
295
296 setAttributes = (element, attributes) ->
297 for attribute, value of attributes
298 element.setAttribute(attribute, value)
299 return
300
301 setHover = (element, hover) ->
302 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
303 domUtils = Cc['@mozilla.org/inspector/dom-utils;1'].getService(Ci.inIDOMUtils)
304 while element.parentElement
305 domUtils[method](element, ':hover')
306 element = element.parentElement
307 return
308
309
310
311 # Language helpers
312
313 class Counter
314 constructor: ({start: @value = 0, @step = 1}) ->
315 tick: -> @value += @step
316
317 class EventEmitter
318 constructor: ->
319 @listeners = {}
320
321 on: (event, listener) ->
322 (@listeners[event] ?= []).push(listener)
323
324 emit: (event, data) ->
325 for listener in @listeners[event] ? []
326 listener(data)
327 return
328
329 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
330
331 nextTick = (window, fn) -> window.setTimeout(fn, 0)
332
333 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
334
335 removeDuplicates = (array) -> Array.from(new Set(array))
336
337 # Remove duplicate characters from string (case insensitive).
338 removeDuplicateCharacters = (str) ->
339 return removeDuplicates( str.toLowerCase().split('') ).join('')
340
341 # Calls `fn` repeatedly, with at least `interval` ms between each call.
342 setInterval = (window, interval, fn) ->
343 stopped = false
344 currentIntervalId = null
345 callback = ->
346 return if stopped
347 currentIntervalId = window.setTimeout((-> fn(callback)), interval)
348 clearInterval = ->
349 stopped = true
350 window.clearTimeout(currentIntervalId)
351 callback()
352 return clearInterval
353
354
355
356 # Misc helpers
357
358 formatError = (error) ->
359 stack = String(error.stack?.formattedStack ? error.stack ? '')
360 .split('\n')
361 .filter((line) -> line.includes('.xpi!'))
362 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
363 .join('\n')
364 return "#{error}\n#{stack}"
365
366 getCurrentLocation = ->
367 window = getCurrentWindow()
368 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
369
370 getCurrentWindow = ->
371 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
372 .getService(Components.interfaces.nsIWindowMediator)
373 return windowMediator.getMostRecentWindow('navigator:browser')
374
375 loadCss = (name) ->
376 sss = Cc['@mozilla.org/content/style-sheet-service;1']
377 .getService(Ci.nsIStyleSheetService)
378 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
379 method = sss.AUTHOR_SHEET
380 unless sss.sheetRegistered(uri, method)
381 sss.loadAndRegisterSheet(uri, method)
382 module.onShutdown(->
383 sss.unregisterSheet(uri, method)
384 )
385
386 observe = (topic, observer) ->
387 observer = {observe: observer} if typeof observer == 'function'
388 Services.obs.addObserver(observer, topic, false)
389 module.onShutdown(->
390 Services.obs.removeObserver(observer, topic, false)
391 )
392
393 openPopup = (popup) ->
394 window = popup.ownerGlobal
395 # Show the popup so it gets a height and width.
396 popup.openPopupAtScreen(0, 0)
397 # Center the popup inside the window.
398 popup.moveTo(
399 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
400 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
401 )
402
403 openTab = (window, url, options) ->
404 {gBrowser} = window
405 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
406 gBrowser.loadOneTab(url, options)
407
408 writeToClipboard = (text) ->
409 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
410 .getService(Ci.nsIClipboardHelper)
411 clipboardHelper.copyString(text)
412
413
414
415 module.exports = {
416 isActivatable
417 isAdjustable
418 isContentEditable
419 isProperLink
420 isTextInputElement
421 isTypingElement
422
423 getActiveElement
424 blurActiveElement
425 blurActiveBrowserElement
426 focusElement
427 getFocusType
428
429 listen
430 listenOnce
431 onRemoved
432 suppressEvent
433 simulateMouseEvents
434
435 area
436 containsDeep
437 createBox
438 injectTemporaryPopup
439 insertText
440 querySelectorAllDeep
441 scroll
442 setAttributes
443 setHover
444
445 Counter
446 EventEmitter
447 has
448 nextTick
449 regexEscape
450 removeDuplicates
451 removeDuplicateCharacters
452 setInterval
453
454 formatError
455 getCurrentLocation
456 getCurrentWindow
457 loadCss
458 observe
459 openPopup
460 openTab
461 writeToClipboard
462 }
Imprint / Impressum