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