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