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