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