]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix error in the 'underflow' event
[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 moveFocus = (direction) ->
132 focusManager = Cc['@mozilla.org/focus-manager;1']
133 .getService(Ci.nsIFocusManager)
134 directionFlag =
135 if direction == -1
136 focusManager.MOVEFOCUS_BACKWARD
137 else
138 focusManager.MOVEFOCUS_FORWARD
139 focusManager.moveFocus(
140 null, # Use current window.
141 null, # Move relative to the currently focused element.
142 directionFlag,
143 focusManager.FLAG_BYKEY
144 )
145
146 getFocusType = (event) ->
147 target = event.originalTarget
148 return switch
149 when isTextInputElement(target) or isContentEditable(target)
150 'editable'
151 when isActivatable(target)
152 'activatable'
153 when isAdjustable(target)
154 'adjustable'
155 else
156 null
157
158
159
160 # Event helpers
161
162 listen = (element, eventName, listener, useCapture = true) ->
163 element.addEventListener(eventName, listener, useCapture)
164 module.onShutdown(->
165 element.removeEventListener(eventName, listener, useCapture)
166 )
167
168 listenOnce = (element, eventName, listener, useCapture = true) ->
169 fn = (event) ->
170 listener(event)
171 element.removeEventListener(eventName, fn, useCapture)
172 listen(element, eventName, fn, useCapture)
173
174 onRemoved = (window, element, fn) ->
175 mutationObserver = new window.MutationObserver((changes) ->
176 for change in changes then for removedElement in change.removedNodes
177 if removedElement == element
178 mutationObserver.disconnect()
179 fn()
180 return
181 )
182 mutationObserver.observe(element.parentNode, {childList: true})
183 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
184
185 suppressEvent = (event) ->
186 event.preventDefault()
187 event.stopPropagation()
188
189 # Simulate mouse click with a full chain of events. ('command' is for XUL
190 # elements.)
191 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
192 simulateClick = (element) ->
193 window = element.ownerDocument.defaultView
194 for type in eventSequence
195 mouseEvent = new window.MouseEvent(type, {
196 # Let the event bubble in order to trigger delegated event listeners.
197 bubbles: true
198 # Make the event cancelable so that `<a href="#">` can be used as a
199 # JavaScript-powered button without scrolling to the top of the page.
200 cancelable: true
201 })
202 element.dispatchEvent(mouseEvent)
203 return
204
205
206
207 # DOM helpers
208
209 area = (element) ->
210 return element.clientWidth * element.clientHeight
211
212 createBox = (document, className, parent = null, text = null) ->
213 box = document.createElement('box')
214 box.className = className
215 box.textContent = text if text?
216 parent.appendChild(box) if parent?
217 return box
218
219 insertText = (input, value) ->
220 { selectionStart, selectionEnd } = input
221 input.value =
222 input.value[0...selectionStart] + value + input.value[selectionEnd..]
223 input.selectionStart = input.selectionEnd = selectionStart + value.length
224
225 querySelectorAllDeep = (window, selector) ->
226 elements = Array.from(window.document.querySelectorAll(selector))
227 for frame in window.frames
228 elements.push(querySelectorAllDeep(frame, selector)...)
229 return elements
230
231 setAttributes = (element, attributes) ->
232 for attribute, value of attributes
233 element.setAttribute(attribute, value)
234 return
235
236 windowContainsDeep = (window, element) ->
237 parent = element.ownerDocument.defaultView
238 loop
239 return true if parent == window
240 break if parent.top == parent
241 parent = parent.parent
242 return false
243
244
245
246 # Language helpers
247
248 class Counter
249 constructor: ({ start: @value = 0, @step = 1 }) ->
250 tick: -> @value += @step
251
252 class EventEmitter
253 constructor: ->
254 @listeners = {}
255
256 on: (event, listener) ->
257 (@listeners[event] ?= []).push(listener)
258
259 emit: (event, data) ->
260 for listener in @listeners[event] ? []
261 listener(data)
262 return
263
264 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
265
266 nextTick = (window, fn) -> window.setTimeout(fn, 0)
267
268 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
269
270 removeDuplicates = (array) -> Array.from(new Set(array))
271
272 # Remove duplicate characters from string (case insensitive).
273 removeDuplicateCharacters = (str) ->
274 return removeDuplicates( str.toLowerCase().split('') ).join('')
275
276
277
278 # Misc helpers
279
280 formatError = (error) ->
281 stack = String(error.stack?.formattedStack ? error.stack ? '')
282 .split('\n')
283 .filter((line) -> line.includes('.xpi!'))
284 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
285 .join('\n')
286 return "#{ error }\n#{ stack }"
287
288 getCurrentLocation = ->
289 window = getCurrentWindow()
290 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
291
292 getCurrentWindow = ->
293 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
294 .getService(Components.interfaces.nsIWindowMediator)
295 return windowMediator.getMostRecentWindow('navigator:browser')
296
297 loadCss = (name) ->
298 sss = Cc['@mozilla.org/content/style-sheet-service;1']
299 .getService(Ci.nsIStyleSheetService)
300 uri = Services.io.newURI("chrome://vimfx/skin/#{ name }.css", null, null)
301 method = sss.AUTHOR_SHEET
302 unless sss.sheetRegistered(uri, method)
303 sss.loadAndRegisterSheet(uri, method)
304 module.onShutdown(->
305 sss.unregisterSheet(uri, method)
306 )
307
308 observe = (topic, observer) ->
309 observer = {observe: observer} if typeof observer == 'function'
310 Services.obs.addObserver(observer, topic, false)
311 module.onShutdown(->
312 Services.obs.removeObserver(observer, topic, false)
313 )
314
315 openTab = (window, url, options) ->
316 { gBrowser } = window
317 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
318 gBrowser.loadOneTab(url, options)
319
320 writeToClipboard = (text) ->
321 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
322 .getService(Ci.nsIClipboardHelper)
323 clipboardHelper.copyString(text)
324
325
326
327 module.exports = {
328 isActivatable
329 isAdjustable
330 isContentEditable
331 isProperLink
332 isTextInputElement
333 isTypingElement
334
335 getActiveElement
336 blurActiveElement
337 blurActiveBrowserElement
338 focusElement
339 moveFocus
340 getFocusType
341
342 listen
343 listenOnce
344 onRemoved
345 suppressEvent
346 simulateClick
347
348 area
349 createBox
350 insertText
351 querySelectorAllDeep
352 setAttributes
353 windowContainsDeep
354
355 Counter
356 EventEmitter
357 has
358 nextTick
359 regexEscape
360 removeDuplicates
361 removeDuplicateCharacters
362
363 formatError
364 getCurrentLocation
365 getCurrentWindow
366 loadCss
367 observe
368 openTab
369 writeToClipboard
370 }
Imprint / Impressum