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