]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Reduce unnecessary vertical code alignment
[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' then EVENTS_CLICK
224 when 'hover-start' then EVENTS_HOVER_START
225 when 'hover-end' then EVENTS_HOVER_END
226
227 for type in eventSequence
228 buttonNum = if type in EVENTS_CLICK then 1 else 0
229 mouseEvent = new window.MouseEvent(type, {
230 # Let the event bubble in order to trigger delegated event listeners.
231 bubbles: type not in ['mouseenter', 'mouseleave']
232 # Make the event cancelable so that `<a href="#">` can be used as a
233 # JavaScript-powered button without scrolling to the top of the page.
234 cancelable: type not in ['mouseenter', 'mouseleave']
235 # These properties are just here for mimicing a real click as much as
236 # possible.
237 buttons: buttonNum
238 detail: buttonNum
239 view: window
240 # `page{X,Y}` are set automatically to the correct values when setting
241 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
242 # to set.
243 clientX: rect.left
244 clientY: rect.top
245 # To exactly calculate `screen{X,Y}` one has to to check where the web
246 # page content area is inside the browser chrome and go through all parent
247 # frames as well. This is good enough. YAGNI for now.
248 screenX: window.screenX + rect.left
249 screenY: window.screenY + rect.top
250 })
251 element.dispatchEvent(mouseEvent)
252
253 return
254
255
256
257 # DOM helpers
258
259 area = (element) ->
260 return element.clientWidth * element.clientHeight
261
262 containsDeep = (parent, element) ->
263 parentWindow = parent.ownerGlobal
264 elementWindow = element.ownerGlobal
265
266 # Owner windows might be missing when opening the devtools.
267 while elementWindow and parentWindow and
268 elementWindow != parentWindow and elementWindow.top != elementWindow
269 element = elementWindow.frameElement
270 elementWindow = element.ownerGlobal
271
272 return parent.contains(element)
273
274 createBox = (document, className = '', parent = null, text = null) ->
275 box = document.createElement('box')
276 box.className = "#{className} vimfx-box"
277 box.textContent = text if text?
278 parent.appendChild(box) if parent?
279 return box
280
281 injectTemporaryPopup = (document, contents) ->
282 popup = document.createElement('menupopup')
283 popup.appendChild(contents)
284 document.getElementById('mainPopupSet').appendChild(popup)
285 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
286 return popup
287
288 insertText = (input, value) ->
289 {selectionStart, selectionEnd} = input
290 input.value =
291 input.value[0...selectionStart] + value + input.value[selectionEnd..]
292 input.selectionStart = input.selectionEnd = selectionStart + value.length
293
294 querySelectorAllDeep = (window, selector) ->
295 elements = Array.from(window.document.querySelectorAll(selector))
296 for frame in window.frames
297 elements.push(querySelectorAllDeep(frame, selector)...)
298 return elements
299
300 scroll = (element, args) ->
301 {method, type, directions, amounts, properties, adjustment, smooth} = args
302 options = {}
303 for direction, index in directions
304 amount = amounts[index]
305 options[direction] = -Math.sign(amount) * adjustment + switch type
306 when 'lines' then amount
307 when 'pages' then amount * element[properties[index]]
308 when 'other' then Math.min(amount, element[properties[index]])
309 options.behavior = 'smooth' if smooth
310 element[method](options)
311
312 setAttributes = (element, attributes) ->
313 for attribute, value of attributes
314 element.setAttribute(attribute, value)
315 return
316
317 setHover = (element, hover) ->
318 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
319 while element.parentElement
320 nsIDomUtils[method](element, ':hover')
321 element = element.parentElement
322 return
323
324
325
326 # Language helpers
327
328 class Counter
329 constructor: ({start: @value = 0, @step = 1}) ->
330 tick: -> @value += @step
331
332 class EventEmitter
333 constructor: ->
334 @listeners = {}
335
336 on: (event, listener) ->
337 (@listeners[event] ?= []).push(listener)
338
339 emit: (event, data) ->
340 for listener in @listeners[event] ? []
341 listener(data)
342 return
343
344 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
345
346 # Check if `search` exists in `string` (case insensitively). Returns `false` if
347 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
348 includes = (string, search) ->
349 return false unless typeof string == 'string'
350 return string.toLowerCase().includes(search)
351
352
353 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
354
355 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
356
357 removeDuplicates = (array) -> Array.from(new Set(array))
358
359 # Remove duplicate characters from string (case insensitive).
360 removeDuplicateCharacters = (str) ->
361 return removeDuplicates( str.toLowerCase().split('') ).join('')
362
363 # Calls `fn` repeatedly, with at least `interval` ms between each call.
364 interval = (window, interval, fn) ->
365 stopped = false
366 currentIntervalId = null
367 next = ->
368 return if stopped
369 currentIntervalId = window.setTimeout((-> fn(next)), interval)
370 clearInterval = ->
371 stopped = true
372 window.clearTimeout(currentIntervalId)
373 next()
374 return clearInterval
375
376
377
378 # Misc helpers
379
380 formatError = (error) ->
381 stack = String(error.stack?.formattedStack ? error.stack ? '')
382 .split('\n')
383 .filter((line) -> line.includes('.xpi!'))
384 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
385 .join('\n')
386 return "#{error}\n#{stack}"
387
388 getCurrentLocation = ->
389 window = getCurrentWindow()
390 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
391
392 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
393
394 hasEventListeners = (element, type) ->
395 for listener in nsIEventListenerService.getListenerInfoFor(element)
396 if listener.listenerObject and listener.type == type
397 return true
398 return false
399
400 loadCss = (name) ->
401 uri = Services.io.newURI("#{ADDON_PATH}/skin/#{name}.css", null, null)
402 method = nsIStyleSheetService.AUTHOR_SHEET
403 unless nsIStyleSheetService.sheetRegistered(uri, method)
404 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
405 module.onShutdown(->
406 nsIStyleSheetService.unregisterSheet(uri, method)
407 )
408
409 observe = (topic, observer) ->
410 observer = {observe: observer} if typeof observer == 'function'
411 Services.obs.addObserver(observer, topic, false)
412 module.onShutdown(->
413 Services.obs.removeObserver(observer, topic, false)
414 )
415
416 openPopup = (popup) ->
417 window = popup.ownerGlobal
418 # Show the popup so it gets a height and width.
419 popup.openPopupAtScreen(0, 0)
420 # Center the popup inside the window.
421 popup.moveTo(
422 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
423 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
424 )
425
426 openTab = (window, url, options) ->
427 {gBrowser} = window
428 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
429 gBrowser.loadOneTab(url, options)
430
431 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
432
433
434
435 module.exports = {
436 isActivatable
437 isAdjustable
438 isContentEditable
439 isProperLink
440 isTextInputElement
441 isTypingElement
442
443 getActiveElement
444 blurActiveElement
445 blurActiveBrowserElement
446 focusElement
447 getFocusType
448
449 listen
450 listenOnce
451 onRemoved
452 suppressEvent
453 simulateMouseEvents
454
455 area
456 containsDeep
457 createBox
458 injectTemporaryPopup
459 insertText
460 querySelectorAllDeep
461 scroll
462 setAttributes
463 setHover
464
465 Counter
466 EventEmitter
467 has
468 includes
469 nextTick
470 regexEscape
471 removeDuplicates
472 removeDuplicateCharacters
473 interval
474
475 formatError
476 getCurrentLocation
477 getCurrentWindow
478 hasEventListeners
479 loadCss
480 observe
481 openPopup
482 openTab
483 writeToClipboard
484 }
Imprint / Impressum