]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Remove outdated locale: el-GR
[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 XULDocument = Ci.nsIDOMXULDocument
30 XULButtonElement = Ci.nsIDOMXULButtonElement
31 XULControlElement = Ci.nsIDOMXULControlElement
32 XULMenuListElement = Ci.nsIDOMXULMenuListElement
33 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
34
35 USE_CAPTURE = true
36
37
38
39 # Element classification helpers
40
41 isActivatable = (element) ->
42 return element instanceof HTMLAnchorElement or
43 element instanceof HTMLButtonElement or
44 (element instanceof HTMLInputElement and element.type in [
45 'button', 'submit', 'reset', 'image'
46 ]) or
47 element instanceof XULButtonElement
48
49 isAdjustable = (element) ->
50 return element instanceof HTMLInputElement and element.type in [
51 'checkbox', 'radio', 'file', 'color'
52 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
53 ] or
54 element instanceof XULControlElement or
55 # Youtube special case.
56 element.classList?.contains('html5-video-player') or
57 element.classList?.contains('ytp-button')
58
59 isContentEditable = (element) ->
60 return element.isContentEditable or
61 # `g_editable` is a non-standard attribute commonly used by Google.
62 element.getAttribute?('g_editable') == 'true' or
63 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
64
65 isProperLink = (element) ->
66 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
67 # href="">`s used as buttons on some sites.
68 return element.getAttribute('href') and
69 (element instanceof HTMLAnchorElement or
70 element.ownerDocument instanceof XULDocument) and
71 not element.href.endsWith('#') and
72 not element.href.endsWith('#?') and
73 not element.href.startsWith('javascript:')
74
75 isTextInputElement = (element) ->
76 return (element instanceof HTMLInputElement and element.type in [
77 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
78 ]) or
79 element instanceof HTMLTextAreaElement or
80 element instanceof XULTextBoxElement
81
82 isTypingElement = (element) ->
83 return isTextInputElement(element) or
84 # `<select>` elements can also receive text input: You may type the
85 # text of an item to select it.
86 element instanceof HTMLSelectElement or
87 element instanceof XULMenuListElement
88
89
90
91 # Active/focused element helpers
92
93 getActiveElement = (window) ->
94 {activeElement} = window.document
95 # If the active element is a frame, recurse into it. The easiest way to detect
96 # a frame that works both in browser UI and in web page content is to check
97 # for the presence of `.contentWindow`. However, in non-multi-process
98 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
99 # pointing to the web page content `window`, which we don’t want to recurse
100 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
101 # to be the only way to detect such elements.
102 if activeElement.localName != 'browser' and activeElement.contentWindow
103 return getActiveElement(activeElement.contentWindow)
104 else
105 return activeElement
106
107 blurActiveElement = (window) ->
108 return unless activeElement = getActiveElement(window)
109 activeElement.blur()
110
111 blurActiveBrowserElement = (vim) ->
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 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
129 # look more appropriate, but it unconditionally selects all text, which
130 # `.FLAG_BYMOUSE` does not.
131 focusElement = (element, options = {}) ->
132 focusManager = Cc['@mozilla.org/focus-manager;1']
133 .getService(Ci.nsIFocusManager)
134 focusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
135 element.select?() if options.select
136
137 getFocusType = (element) -> switch
138 when isTypingElement(element) or isContentEditable(element)
139 'editable'
140 when isActivatable(element)
141 'activatable'
142 when isAdjustable(element)
143 'adjustable'
144 else
145 null
146
147
148
149 # Event helpers
150
151 listen = (element, eventName, listener, useCapture = true) ->
152 element.addEventListener(eventName, listener, useCapture)
153 module.onShutdown(->
154 element.removeEventListener(eventName, listener, useCapture)
155 )
156
157 listenOnce = (element, eventName, listener, useCapture = true) ->
158 fn = (event) ->
159 listener(event)
160 element.removeEventListener(eventName, fn, useCapture)
161 listen(element, eventName, fn, useCapture)
162
163 onRemoved = (window, element, fn) ->
164 mutationObserver = new window.MutationObserver((changes) ->
165 for change in changes then for removedElement in change.removedNodes
166 if removedElement == element
167 mutationObserver.disconnect()
168 fn()
169 return
170 )
171 mutationObserver.observe(element.parentNode, {childList: true})
172 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
173
174 suppressEvent = (event) ->
175 event.preventDefault()
176 event.stopPropagation()
177
178 # Simulate mouse click with a full chain of events. ('command' is for XUL
179 # elements.)
180 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
181 simulateClick = (element) ->
182 window = element.ownerGlobal
183 for type in eventSequence
184 mouseEvent = new window.MouseEvent(type, {
185 # Let the event bubble in order to trigger delegated event listeners.
186 bubbles: true
187 # Make the event cancelable so that `<a href="#">` can be used as a
188 # JavaScript-powered button without scrolling to the top of the page.
189 cancelable: true
190 })
191 element.dispatchEvent(mouseEvent)
192 return
193
194
195
196 # DOM helpers
197
198 area = (element) ->
199 return element.clientWidth * element.clientHeight
200
201 containsDeep = (parent, element) ->
202 parentWindow = parent.ownerGlobal
203 elementWindow = element.ownerGlobal
204
205 while elementWindow != parentWindow and elementWindow.top != elementWindow
206 element = elementWindow.frameElement
207 elementWindow = element.ownerGlobal
208
209 return parent.contains(element)
210
211 createBox = (document, className = '', parent = null, text = null) ->
212 box = document.createElement('box')
213 box.className = "#{className} vimfx-box"
214 box.textContent = text if text?
215 parent.appendChild(box) if parent?
216 return box
217
218 injectTemporaryPopup = (document, contents) ->
219 popup = document.createElement('menupopup')
220 popup.appendChild(contents)
221 document.getElementById('mainPopupSet').appendChild(popup)
222 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
223 return popup
224
225 insertText = (input, value) ->
226 {selectionStart, selectionEnd} = input
227 input.value =
228 input.value[0...selectionStart] + value + input.value[selectionEnd..]
229 input.selectionStart = input.selectionEnd = selectionStart + value.length
230
231 querySelectorAllDeep = (window, selector) ->
232 elements = Array.from(window.document.querySelectorAll(selector))
233 for frame in window.frames
234 elements.push(querySelectorAllDeep(frame, selector)...)
235 return elements
236
237 setAttributes = (element, attributes) ->
238 for attribute, value of attributes
239 element.setAttribute(attribute, value)
240 return
241
242
243
244 # Language helpers
245
246 class Counter
247 constructor: ({start: @value = 0, @step = 1}) ->
248 tick: -> @value += @step
249
250 class EventEmitter
251 constructor: ->
252 @listeners = {}
253
254 on: (event, listener) ->
255 (@listeners[event] ?= []).push(listener)
256
257 emit: (event, data) ->
258 for listener in @listeners[event] ? []
259 listener(data)
260 return
261
262 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
263
264 nextTick = (window, fn) -> window.setTimeout(fn, 0)
265
266 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
267
268 removeDuplicates = (array) -> Array.from(new Set(array))
269
270 # Remove duplicate characters from string (case insensitive).
271 removeDuplicateCharacters = (str) ->
272 return removeDuplicates( str.toLowerCase().split('') ).join('')
273
274
275
276 # Misc helpers
277
278 formatError = (error) ->
279 stack = String(error.stack?.formattedStack ? error.stack ? '')
280 .split('\n')
281 .filter((line) -> line.includes('.xpi!'))
282 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
283 .join('\n')
284 return "#{error}\n#{stack}"
285
286 getCurrentLocation = ->
287 window = getCurrentWindow()
288 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
289
290 getCurrentWindow = ->
291 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
292 .getService(Components.interfaces.nsIWindowMediator)
293 return windowMediator.getMostRecentWindow('navigator:browser')
294
295 loadCss = (name) ->
296 sss = Cc['@mozilla.org/content/style-sheet-service;1']
297 .getService(Ci.nsIStyleSheetService)
298 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
299 method = sss.AUTHOR_SHEET
300 unless sss.sheetRegistered(uri, method)
301 sss.loadAndRegisterSheet(uri, method)
302 module.onShutdown(->
303 sss.unregisterSheet(uri, method)
304 )
305
306 observe = (topic, observer) ->
307 observer = {observe: observer} if typeof observer == 'function'
308 Services.obs.addObserver(observer, topic, false)
309 module.onShutdown(->
310 Services.obs.removeObserver(observer, topic, false)
311 )
312
313 openPopup = (popup) ->
314 window = popup.ownerGlobal
315 # Show the popup so it gets a height and width.
316 popup.openPopupAtScreen(0, 0)
317 # Center the popup inside the window.
318 popup.moveTo(
319 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
320 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
321 )
322
323 openTab = (window, url, options) ->
324 {gBrowser} = window
325 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
326 gBrowser.loadOneTab(url, options)
327
328 writeToClipboard = (text) ->
329 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
330 .getService(Ci.nsIClipboardHelper)
331 clipboardHelper.copyString(text)
332
333
334
335 module.exports = {
336 isActivatable
337 isAdjustable
338 isContentEditable
339 isProperLink
340 isTextInputElement
341 isTypingElement
342
343 getActiveElement
344 blurActiveElement
345 blurActiveBrowserElement
346 focusElement
347 getFocusType
348
349 listen
350 listenOnce
351 onRemoved
352 suppressEvent
353 simulateClick
354
355 area
356 containsDeep
357 createBox
358 injectTemporaryPopup
359 insertText
360 querySelectorAllDeep
361 setAttributes
362
363 Counter
364 EventEmitter
365 has
366 nextTick
367 regexEscape
368 removeDuplicates
369 removeDuplicateCharacters
370
371 formatError
372 getCurrentLocation
373 getCurrentWindow
374 loadCss
375 observe
376 openPopup
377 openTab
378 writeToClipboard
379 }
Imprint / Impressum