]> 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 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:')
74
75 isTextInputElement = (element) ->
76 return (element instanceof HTMLInputElement and element.type in [
77 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
78 ]) or
79 element instanceof HTMLTextAreaElement or
80 element instanceof XULTextBoxElement
81
82 isTypingElement = (element) ->
83 return isTextInputElement(element) or
84 # `<select>` elements can also receive text input: You may type the
85 # text of an item to select it.
86 element instanceof HTMLSelectElement or
87 element instanceof XULMenuListElement
88
89
90
91 # Active/focused element helpers
92
93 getActiveElement = (window) ->
94 {activeElement} = window.document
95 if activeElement.contentWindow
96 return getActiveElement(activeElement.contentWindow)
97 else
98 return activeElement
99
100 blurActiveElement = (window) ->
101 return unless activeElement = getActiveElement(window)
102 activeElement.blur()
103
104 blurActiveBrowserElement = (vim) ->
105 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
106 # reset it, for example.
107 # - Focusing the current browser afterwards allows to pass `<escape>` as well
108 # as unbound keys to the page. However, focusing the browser also triggers
109 # focus events on `document` and `window` in the current page. Many pages
110 # re-focus some text input on those events, making it impossible to blur
111 # those! Therefore we tell the frame script to suppress those events.
112 {window} = vim
113 activeElement = getActiveElement(window)
114 vim._send('browserRefocus')
115 nextTick(window, ->
116 activeElement.blur()
117 window.gBrowser.selectedBrowser.focus()
118 )
119
120 # Focus an element and tell Firefox that the focus happened because of a user
121 # keypress (not just because some random programmatic focus).
122 focusElement = (element, options = {}) ->
123 focusManager = Cc['@mozilla.org/focus-manager;1']
124 .getService(Ci.nsIFocusManager)
125 focusManager.setFocus(element, focusManager.FLAG_BYKEY)
126 element.select?() if options.select
127
128 getFocusType = (element) -> switch
129 when isTypingElement(element) or isContentEditable(element)
130 'editable'
131 when isActivatable(element)
132 'activatable'
133 when isAdjustable(element)
134 'adjustable'
135 else
136 null
137
138
139
140 # Event helpers
141
142 listen = (element, eventName, listener, useCapture = true) ->
143 element.addEventListener(eventName, listener, useCapture)
144 module.onShutdown(->
145 element.removeEventListener(eventName, listener, useCapture)
146 )
147
148 listenOnce = (element, eventName, listener, useCapture = true) ->
149 fn = (event) ->
150 listener(event)
151 element.removeEventListener(eventName, fn, useCapture)
152 listen(element, eventName, fn, useCapture)
153
154 onRemoved = (window, element, fn) ->
155 mutationObserver = new window.MutationObserver((changes) ->
156 for change in changes then for removedElement in change.removedNodes
157 if removedElement == element
158 mutationObserver.disconnect()
159 fn()
160 return
161 )
162 mutationObserver.observe(element.parentNode, {childList: true})
163 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
164
165 suppressEvent = (event) ->
166 event.preventDefault()
167 event.stopPropagation()
168
169 # Simulate mouse click with a full chain of events. ('command' is for XUL
170 # elements.)
171 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
172 simulateClick = (element) ->
173 window = element.ownerDocument.defaultView
174 for type in eventSequence
175 mouseEvent = new window.MouseEvent(type, {
176 # Let the event bubble in order to trigger delegated event listeners.
177 bubbles: true
178 # Make the event cancelable so that `<a href="#">` can be used as a
179 # JavaScript-powered button without scrolling to the top of the page.
180 cancelable: true
181 })
182 element.dispatchEvent(mouseEvent)
183 return
184
185
186
187 # DOM helpers
188
189 area = (element) ->
190 return element.clientWidth * element.clientHeight
191
192 createBox = (document, className, parent = null, text = null) ->
193 box = document.createElement('box')
194 box.className = className
195 box.textContent = text if text?
196 parent.appendChild(box) if parent?
197 return box
198
199 insertText = (input, value) ->
200 {selectionStart, selectionEnd} = input
201 input.value =
202 input.value[0...selectionStart] + value + input.value[selectionEnd..]
203 input.selectionStart = input.selectionEnd = selectionStart + value.length
204
205 querySelectorAllDeep = (window, selector) ->
206 elements = Array.from(window.document.querySelectorAll(selector))
207 for frame in window.frames
208 elements.push(querySelectorAllDeep(frame, selector)...)
209 return elements
210
211 setAttributes = (element, attributes) ->
212 for attribute, value of attributes
213 element.setAttribute(attribute, value)
214 return
215
216
217
218 # Language helpers
219
220 class Counter
221 constructor: ({start: @value = 0, @step = 1}) ->
222 tick: -> @value += @step
223
224 class EventEmitter
225 constructor: ->
226 @listeners = {}
227
228 on: (event, listener) ->
229 (@listeners[event] ?= []).push(listener)
230
231 emit: (event, data) ->
232 for listener in @listeners[event] ? []
233 listener(data)
234 return
235
236 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
237
238 nextTick = (window, fn) -> window.setTimeout(fn, 0)
239
240 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
241
242 removeDuplicates = (array) -> Array.from(new Set(array))
243
244 # Remove duplicate characters from string (case insensitive).
245 removeDuplicateCharacters = (str) ->
246 return removeDuplicates( str.toLowerCase().split('') ).join('')
247
248
249
250 # Misc helpers
251
252 formatError = (error) ->
253 stack = String(error.stack?.formattedStack ? error.stack ? '')
254 .split('\n')
255 .filter((line) -> line.includes('.xpi!'))
256 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
257 .join('\n')
258 return "#{error}\n#{stack}"
259
260 getCurrentLocation = ->
261 window = getCurrentWindow()
262 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
263
264 getCurrentWindow = ->
265 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
266 .getService(Components.interfaces.nsIWindowMediator)
267 return windowMediator.getMostRecentWindow('navigator:browser')
268
269 loadCss = (name) ->
270 sss = Cc['@mozilla.org/content/style-sheet-service;1']
271 .getService(Ci.nsIStyleSheetService)
272 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
273 method = sss.AUTHOR_SHEET
274 unless sss.sheetRegistered(uri, method)
275 sss.loadAndRegisterSheet(uri, method)
276 module.onShutdown(->
277 sss.unregisterSheet(uri, method)
278 )
279
280 observe = (topic, observer) ->
281 observer = {observe: observer} if typeof observer == 'function'
282 Services.obs.addObserver(observer, topic, false)
283 module.onShutdown(->
284 Services.obs.removeObserver(observer, topic, false)
285 )
286
287 openTab = (window, url, options) ->
288 {gBrowser} = window
289 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
290 gBrowser.loadOneTab(url, options)
291
292 writeToClipboard = (text) ->
293 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
294 .getService(Ci.nsIClipboardHelper)
295 clipboardHelper.copyString(text)
296
297
298
299 module.exports = {
300 isActivatable
301 isAdjustable
302 isContentEditable
303 isProperLink
304 isTextInputElement
305 isTypingElement
306
307 getActiveElement
308 blurActiveElement
309 blurActiveBrowserElement
310 focusElement
311 getFocusType
312
313 listen
314 listenOnce
315 onRemoved
316 suppressEvent
317 simulateClick
318
319 area
320 createBox
321 insertText
322 querySelectorAllDeep
323 setAttributes
324
325 Counter
326 EventEmitter
327 has
328 nextTick
329 regexEscape
330 removeDuplicates
331 removeDuplicateCharacters
332
333 formatError
334 getCurrentLocation
335 getCurrentWindow
336 loadCss
337 observe
338 openTab
339 writeToClipboard
340 }
Imprint / Impressum