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