]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix the empty notification for the `gH` command
[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 # Owner windows might be missing when opening the devtools.
253 while elementWindow and parentWindow and
254 elementWindow != parentWindow and elementWindow.top != elementWindow
255 element = elementWindow.frameElement
256 elementWindow = element.ownerGlobal
257
258 return parent.contains(element)
259
260 createBox = (document, className = '', parent = null, text = null) ->
261 box = document.createElement('box')
262 box.className = "#{className} vimfx-box"
263 box.textContent = text if text?
264 parent.appendChild(box) if parent?
265 return box
266
267 injectTemporaryPopup = (document, contents) ->
268 popup = document.createElement('menupopup')
269 popup.appendChild(contents)
270 document.getElementById('mainPopupSet').appendChild(popup)
271 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
272 return popup
273
274 insertText = (input, value) ->
275 {selectionStart, selectionEnd} = input
276 input.value =
277 input.value[0...selectionStart] + value + input.value[selectionEnd..]
278 input.selectionStart = input.selectionEnd = selectionStart + value.length
279
280 querySelectorAllDeep = (window, selector) ->
281 elements = Array.from(window.document.querySelectorAll(selector))
282 for frame in window.frames
283 elements.push(querySelectorAllDeep(frame, selector)...)
284 return elements
285
286 scroll = (element, args) ->
287 {method, type, directions, amounts, properties, adjustment, smooth} = args
288 options = {}
289 for direction, index in directions
290 amount = amounts[index]
291 options[direction] = -Math.sign(amount) * adjustment + switch type
292 when 'lines' then amount
293 when 'pages' then amount * element[properties[index]]
294 when 'other' then Math.min(amount, element[properties[index]])
295 options.behavior = 'smooth' if smooth
296 element[method](options)
297
298 setAttributes = (element, attributes) ->
299 for attribute, value of attributes
300 element.setAttribute(attribute, value)
301 return
302
303 setHover = (element, hover) ->
304 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
305 domUtils = Cc['@mozilla.org/inspector/dom-utils;1'].getService(Ci.inIDOMUtils)
306 while element.parentElement
307 domUtils[method](element, ':hover')
308 element = element.parentElement
309 return
310
311
312
313 # Language helpers
314
315 class Counter
316 constructor: ({start: @value = 0, @step = 1}) ->
317 tick: -> @value += @step
318
319 class EventEmitter
320 constructor: ->
321 @listeners = {}
322
323 on: (event, listener) ->
324 (@listeners[event] ?= []).push(listener)
325
326 emit: (event, data) ->
327 for listener in @listeners[event] ? []
328 listener(data)
329 return
330
331 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
332
333 nextTick = (window, fn) -> window.setTimeout(fn, 0)
334
335 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
336
337 removeDuplicates = (array) -> Array.from(new Set(array))
338
339 # Remove duplicate characters from string (case insensitive).
340 removeDuplicateCharacters = (str) ->
341 return removeDuplicates( str.toLowerCase().split('') ).join('')
342
343 # Calls `fn` repeatedly, with at least `interval` ms between each call.
344 setInterval = (window, interval, fn) ->
345 stopped = false
346 currentIntervalId = null
347 next = ->
348 return if stopped
349 currentIntervalId = window.setTimeout((-> fn(next)), interval)
350 clearInterval = ->
351 stopped = true
352 window.clearTimeout(currentIntervalId)
353 next()
354 return clearInterval
355
356
357
358 # Misc helpers
359
360 formatError = (error) ->
361 stack = String(error.stack?.formattedStack ? error.stack ? '')
362 .split('\n')
363 .filter((line) -> line.includes('.xpi!'))
364 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
365 .join('\n')
366 return "#{error}\n#{stack}"
367
368 getCurrentLocation = ->
369 window = getCurrentWindow()
370 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
371
372 getCurrentWindow = ->
373 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
374 .getService(Components.interfaces.nsIWindowMediator)
375 return windowMediator.getMostRecentWindow('navigator:browser')
376
377 loadCss = (name) ->
378 sss = Cc['@mozilla.org/content/style-sheet-service;1']
379 .getService(Ci.nsIStyleSheetService)
380 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
381 method = sss.AUTHOR_SHEET
382 unless sss.sheetRegistered(uri, method)
383 sss.loadAndRegisterSheet(uri, method)
384 module.onShutdown(->
385 sss.unregisterSheet(uri, method)
386 )
387
388 observe = (topic, observer) ->
389 observer = {observe: observer} if typeof observer == 'function'
390 Services.obs.addObserver(observer, topic, false)
391 module.onShutdown(->
392 Services.obs.removeObserver(observer, topic, false)
393 )
394
395 openPopup = (popup) ->
396 window = popup.ownerGlobal
397 # Show the popup so it gets a height and width.
398 popup.openPopupAtScreen(0, 0)
399 # Center the popup inside the window.
400 popup.moveTo(
401 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
402 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
403 )
404
405 openTab = (window, url, options) ->
406 {gBrowser} = window
407 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
408 gBrowser.loadOneTab(url, options)
409
410 writeToClipboard = (text) ->
411 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
412 .getService(Ci.nsIClipboardHelper)
413 clipboardHelper.copyString(text)
414
415
416
417 module.exports = {
418 isActivatable
419 isAdjustable
420 isContentEditable
421 isProperLink
422 isTextInputElement
423 isTypingElement
424
425 getActiveElement
426 blurActiveElement
427 blurActiveBrowserElement
428 focusElement
429 getFocusType
430
431 listen
432 listenOnce
433 onRemoved
434 suppressEvent
435 simulateMouseEvents
436
437 area
438 containsDeep
439 createBox
440 injectTemporaryPopup
441 insertText
442 querySelectorAllDeep
443 scroll
444 setAttributes
445 setHover
446
447 Counter
448 EventEmitter
449 has
450 nextTick
451 regexEscape
452 removeDuplicates
453 removeDuplicateCharacters
454 setInterval
455
456 formatError
457 getCurrentLocation
458 getCurrentWindow
459 loadCss
460 observe
461 openPopup
462 openTab
463 writeToClipboard
464 }
Imprint / Impressum