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