]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Improve keyboard shortcut overriding behavior
[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 suppressEvent = (event) ->
137 event.preventDefault()
138 event.stopPropagation()
139
140 # Simulate mouse click with a full chain of events. ('command' is for XUL
141 # elements.)
142 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
143 simulateClick = (element) ->
144 window = element.ownerDocument.defaultView
145 simulatedEvents = {}
146 for type in eventSequence
147 mouseEvent = new window.MouseEvent(type, {
148 # Let the event bubble in order to trigger delegated event listeners.
149 bubbles: true
150 # Make the event cancelable so that `<a href="#">` can be used as a
151 # JavaScript-powered button without scrolling to the top of the page.
152 cancelable: true
153 })
154 element.dispatchEvent(mouseEvent)
155 simulatedEvents[type] = mouseEvent
156 return simulatedEvents
157
158
159
160 # DOM helpers
161
162 area = (element) ->
163 return element.clientWidth * element.clientHeight
164
165 createBox = (document, className, parent = null, text = null) ->
166 box = document.createElement('box')
167 box.className = className
168 box.textContent = text if text?
169 parent.appendChild(box) if parent?
170 return box
171
172 insertText = (input, value) ->
173 { selectionStart, selectionEnd } = input
174 input.value =
175 input.value[0...selectionStart] + value + input.value[selectionEnd..]
176 input.selectionStart = input.selectionEnd = selectionStart + value.length
177
178 setAttributes = (element, attributes) ->
179 for attribute, value of attributes
180 element.setAttribute(attribute, value)
181 return
182
183
184
185 # Language helpers
186
187 class Counter
188 constructor: ({ start: @value = 0, @step = 1 }) ->
189 tick: -> @value += @step
190
191 class EventEmitter
192 constructor: ->
193 @listeners = {}
194
195 on: (event, listener) ->
196 (@listeners[event] ?= []).push(listener)
197
198 emit: (event, data) ->
199 for listener in @listeners[event] ? []
200 listener(data)
201 return
202
203 has = Function::call.bind(Object::hasOwnProperty)
204
205 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
206
207 removeDuplicates = (array) ->
208 # coffeelint: disable=no_backticks
209 return `[...new Set(array)]`
210 # coffeelint: enable=no_backticks
211
212 # Remove duplicate characters from string (case insensitive).
213 removeDuplicateCharacters = (str) ->
214 return removeDuplicates( str.toLowerCase().split('') ).join('')
215
216
217
218 # Misc helpers
219
220 formatError = (error) ->
221 stack = String(error.stack?.formattedStack ? error.stack ? '')
222 .split('\n')
223 .filter((line) -> line.includes('.xpi!'))
224 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
225 .join('\n')
226 return "#{ error }\n#{ stack }"
227
228 getCurrentLocation = (browser = null) ->
229 browser ?= getCurrentWindow().gBrowser.selectedBrowser
230 return new browser.ownerGlobal.URL(browser.currentURI.spec)
231
232 getCurrentWindow = ->
233 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
234 .getService(Components.interfaces.nsIWindowMediator)
235 return windowMediator.getMostRecentWindow('navigator:browser')
236
237 loadCss = (name) ->
238 sss = Cc['@mozilla.org/content/style-sheet-service;1']
239 .getService(Ci.nsIStyleSheetService)
240 uri = Services.io.newURI("chrome://vimfx/skin/#{ name }.css", null, null)
241 method = sss.AUTHOR_SHEET
242 unless sss.sheetRegistered(uri, method)
243 sss.loadAndRegisterSheet(uri, method)
244 module.onShutdown(->
245 sss.unregisterSheet(uri, method)
246 )
247
248 observe = (topic, observer) ->
249 observer = {observe: observer} if typeof observer == 'function'
250 Services.obs.addObserver(observer, topic, false)
251 module.onShutdown(->
252 Services.obs.removeObserver(observer, topic, false)
253 )
254
255 openTab = (window, url, options) ->
256 { gBrowser } = window
257 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
258 gBrowser.loadOneTab(url, options)
259
260 # Executes `fn` and measures how much time it took.
261 timeIt = (fn, name) ->
262 console.time(name)
263 result = fn()
264 console.timeEnd(name)
265 return result
266
267 writeToClipboard = (text) ->
268 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
269 .getService(Ci.nsIClipboardHelper)
270 clipboardHelper.copyString(text)
271
272
273
274 module.exports = {
275 isActivatable
276 isAdjustable
277 isContentEditable
278 isProperLink
279 isTextInputElement
280
281 getActiveElement
282 blurActiveElement
283 focusElement
284 getFocusType
285
286 listen
287 suppressEvent
288 simulateClick
289
290 area
291 createBox
292 insertText
293 setAttributes
294
295 Counter
296 EventEmitter
297 has
298 regexEscape
299 removeDuplicates
300 removeDuplicateCharacters
301
302 formatError
303 getCurrentLocation
304 getCurrentWindow
305 loadCss
306 observe
307 openTab
308 timeIt
309 writeToClipboard
310 }
Imprint / Impressum