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