]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix text selection when forusing text inputs
[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, focusManager.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 insertText = (input, value) ->
219 {selectionStart, selectionEnd} = input
220 input.value =
221 input.value[0...selectionStart] + value + input.value[selectionEnd..]
222 input.selectionStart = input.selectionEnd = selectionStart + value.length
223
224 querySelectorAllDeep = (window, selector) ->
225 elements = Array.from(window.document.querySelectorAll(selector))
226 for frame in window.frames
227 elements.push(querySelectorAllDeep(frame, selector)...)
228 return elements
229
230 setAttributes = (element, attributes) ->
231 for attribute, value of attributes
232 element.setAttribute(attribute, value)
233 return
234
235
236
237 # Language helpers
238
239 class Counter
240 constructor: ({start: @value = 0, @step = 1}) ->
241 tick: -> @value += @step
242
243 class EventEmitter
244 constructor: ->
245 @listeners = {}
246
247 on: (event, listener) ->
248 (@listeners[event] ?= []).push(listener)
249
250 emit: (event, data) ->
251 for listener in @listeners[event] ? []
252 listener(data)
253 return
254
255 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
256
257 nextTick = (window, fn) -> window.setTimeout(fn, 0)
258
259 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
260
261 removeDuplicates = (array) -> Array.from(new Set(array))
262
263 # Remove duplicate characters from string (case insensitive).
264 removeDuplicateCharacters = (str) ->
265 return removeDuplicates( str.toLowerCase().split('') ).join('')
266
267
268
269 # Misc helpers
270
271 formatError = (error) ->
272 stack = String(error.stack?.formattedStack ? error.stack ? '')
273 .split('\n')
274 .filter((line) -> line.includes('.xpi!'))
275 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
276 .join('\n')
277 return "#{error}\n#{stack}"
278
279 getCurrentLocation = ->
280 window = getCurrentWindow()
281 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
282
283 getCurrentWindow = ->
284 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
285 .getService(Components.interfaces.nsIWindowMediator)
286 return windowMediator.getMostRecentWindow('navigator:browser')
287
288 loadCss = (name) ->
289 sss = Cc['@mozilla.org/content/style-sheet-service;1']
290 .getService(Ci.nsIStyleSheetService)
291 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
292 method = sss.AUTHOR_SHEET
293 unless sss.sheetRegistered(uri, method)
294 sss.loadAndRegisterSheet(uri, method)
295 module.onShutdown(->
296 sss.unregisterSheet(uri, method)
297 )
298
299 observe = (topic, observer) ->
300 observer = {observe: observer} if typeof observer == 'function'
301 Services.obs.addObserver(observer, topic, false)
302 module.onShutdown(->
303 Services.obs.removeObserver(observer, topic, false)
304 )
305
306 openTab = (window, url, options) ->
307 {gBrowser} = window
308 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
309 gBrowser.loadOneTab(url, options)
310
311 writeToClipboard = (text) ->
312 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
313 .getService(Ci.nsIClipboardHelper)
314 clipboardHelper.copyString(text)
315
316
317
318 module.exports = {
319 isActivatable
320 isAdjustable
321 isContentEditable
322 isProperLink
323 isTextInputElement
324 isTypingElement
325
326 getActiveElement
327 blurActiveElement
328 blurActiveBrowserElement
329 focusElement
330 getFocusType
331
332 listen
333 listenOnce
334 onRemoved
335 suppressEvent
336 simulateClick
337
338 area
339 containsDeep
340 createBox
341 insertText
342 querySelectorAllDeep
343 setAttributes
344
345 Counter
346 EventEmitter
347 has
348 nextTick
349 regexEscape
350 removeDuplicates
351 removeDuplicateCharacters
352
353 formatError
354 getCurrentLocation
355 getCurrentWindow
356 loadCss
357 observe
358 openTab
359 writeToClipboard
360 }
Imprint / Impressum