]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Treat contenteditable elements as other 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 or
81 isContentEditable(element)
82
83 isTypingElement = (element) ->
84 return isTextInputElement(element) or
85 # `<select>` elements can also receive text input: You may type the
86 # text of an item to select it.
87 element instanceof HTMLSelectElement or
88 element instanceof XULMenuListElement
89
90
91
92 # Active/focused element helpers
93
94 getActiveElement = (window) ->
95 {activeElement} = window.document
96 # If the active element is a frame, recurse into it. The easiest way to detect
97 # a frame that works both in browser UI and in web page content is to check
98 # for the presence of `.contentWindow`. However, in non-multi-process
99 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
100 # pointing to the web page content `window`, which we don’t want to recurse
101 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
102 # to be the only way to detect such elements.
103 if activeElement.localName != 'browser' and activeElement.contentWindow
104 return getActiveElement(activeElement.contentWindow)
105 else
106 return activeElement
107
108 blurActiveElement = (window) ->
109 return unless activeElement = getActiveElement(window)
110 activeElement.blur()
111
112 blurActiveBrowserElement = (vim) ->
113 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
114 # reset it, for example.
115 # - Focusing the current browser afterwards allows to pass `<escape>` as well
116 # as unbound keys to the page. However, focusing the browser also triggers
117 # focus events on `document` and `window` in the current page. Many pages
118 # re-focus some text input on those events, making it impossible to blur
119 # those! Therefore we tell the frame script to suppress those events.
120 {window} = vim
121 activeElement = getActiveElement(window)
122 vim._send('browserRefocus')
123 nextTick(window, ->
124 activeElement.blur()
125 window.gBrowser.selectedBrowser.focus()
126 )
127
128 # Focus an element and tell Firefox that the focus happened because of a user
129 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
130 # look more appropriate, but it unconditionally selects all text, which
131 # `.FLAG_BYMOUSE` does not.
132 focusElement = (element, options = {}) ->
133 focusManager = Cc['@mozilla.org/focus-manager;1']
134 .getService(Ci.nsIFocusManager)
135 focusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
136 element.select?() if options.select
137
138 getFocusType = (element) -> switch
139 when isTypingElement(element)
140 'editable'
141 when isActivatable(element)
142 'activatable'
143 when isAdjustable(element)
144 'adjustable'
145 else
146 null
147
148
149
150 # Event helpers
151
152 listen = (element, eventName, listener, useCapture = true) ->
153 element.addEventListener(eventName, listener, useCapture)
154 module.onShutdown(->
155 element.removeEventListener(eventName, listener, useCapture)
156 )
157
158 listenOnce = (element, eventName, listener, useCapture = true) ->
159 fn = (event) ->
160 listener(event)
161 element.removeEventListener(eventName, fn, useCapture)
162 listen(element, eventName, fn, useCapture)
163
164 onRemoved = (window, element, fn) ->
165 mutationObserver = new window.MutationObserver((changes) ->
166 for change in changes then for removedElement in change.removedNodes
167 if removedElement == element
168 mutationObserver.disconnect()
169 fn()
170 return
171 )
172 mutationObserver.observe(element.parentNode, {childList: true})
173 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
174
175 suppressEvent = (event) ->
176 event.preventDefault()
177 event.stopPropagation()
178
179 # Simulate mouse click with a full chain of events. ('command' is for XUL
180 # elements.)
181 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
182 simulateClick = (element) ->
183 window = element.ownerGlobal
184 for type in eventSequence
185 mouseEvent = new window.MouseEvent(type, {
186 # Let the event bubble in order to trigger delegated event listeners.
187 bubbles: true
188 # Make the event cancelable so that `<a href="#">` can be used as a
189 # JavaScript-powered button without scrolling to the top of the page.
190 cancelable: true
191 })
192 element.dispatchEvent(mouseEvent)
193 return
194
195
196
197 # DOM helpers
198
199 area = (element) ->
200 return element.clientWidth * element.clientHeight
201
202 containsDeep = (parent, element) ->
203 parentWindow = parent.ownerGlobal
204 elementWindow = element.ownerGlobal
205
206 while elementWindow != parentWindow and elementWindow.top != elementWindow
207 element = elementWindow.frameElement
208 elementWindow = element.ownerGlobal
209
210 return parent.contains(element)
211
212 createBox = (document, className = '', parent = null, text = null) ->
213 box = document.createElement('box')
214 box.className = "#{className} vimfx-box"
215 box.textContent = text if text?
216 parent.appendChild(box) if parent?
217 return box
218
219 injectTemporaryPopup = (document, contents) ->
220 popup = document.createElement('menupopup')
221 popup.appendChild(contents)
222 document.getElementById('mainPopupSet').appendChild(popup)
223 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
224 return popup
225
226 insertText = (input, value) ->
227 {selectionStart, selectionEnd} = input
228 input.value =
229 input.value[0...selectionStart] + value + input.value[selectionEnd..]
230 input.selectionStart = input.selectionEnd = selectionStart + value.length
231
232 querySelectorAllDeep = (window, selector) ->
233 elements = Array.from(window.document.querySelectorAll(selector))
234 for frame in window.frames
235 elements.push(querySelectorAllDeep(frame, selector)...)
236 return elements
237
238 scroll = (element, args) ->
239 {method, type, directions, amounts, properties, adjustment, smooth} = args
240 options = {}
241 for direction, index in directions
242 amount = amounts[index]
243 options[direction] = -Math.sign(amount) * adjustment + switch type
244 when 'lines' then amount
245 when 'pages' then amount * element[properties[index]]
246 when 'other' then Math.min(amount, element[properties[index]])
247 options.behavior = 'smooth' if smooth
248 element[method](options)
249
250 setAttributes = (element, attributes) ->
251 for attribute, value of attributes
252 element.setAttribute(attribute, value)
253 return
254
255
256
257 # Language helpers
258
259 class Counter
260 constructor: ({start: @value = 0, @step = 1}) ->
261 tick: -> @value += @step
262
263 class EventEmitter
264 constructor: ->
265 @listeners = {}
266
267 on: (event, listener) ->
268 (@listeners[event] ?= []).push(listener)
269
270 emit: (event, data) ->
271 for listener in @listeners[event] ? []
272 listener(data)
273 return
274
275 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
276
277 nextTick = (window, fn) -> window.setTimeout(fn, 0)
278
279 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
280
281 removeDuplicates = (array) -> Array.from(new Set(array))
282
283 # Remove duplicate characters from string (case insensitive).
284 removeDuplicateCharacters = (str) ->
285 return removeDuplicates( str.toLowerCase().split('') ).join('')
286
287
288
289 # Misc helpers
290
291 formatError = (error) ->
292 stack = String(error.stack?.formattedStack ? error.stack ? '')
293 .split('\n')
294 .filter((line) -> line.includes('.xpi!'))
295 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
296 .join('\n')
297 return "#{error}\n#{stack}"
298
299 getCurrentLocation = ->
300 window = getCurrentWindow()
301 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
302
303 getCurrentWindow = ->
304 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
305 .getService(Components.interfaces.nsIWindowMediator)
306 return windowMediator.getMostRecentWindow('navigator:browser')
307
308 loadCss = (name) ->
309 sss = Cc['@mozilla.org/content/style-sheet-service;1']
310 .getService(Ci.nsIStyleSheetService)
311 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
312 method = sss.AUTHOR_SHEET
313 unless sss.sheetRegistered(uri, method)
314 sss.loadAndRegisterSheet(uri, method)
315 module.onShutdown(->
316 sss.unregisterSheet(uri, method)
317 )
318
319 observe = (topic, observer) ->
320 observer = {observe: observer} if typeof observer == 'function'
321 Services.obs.addObserver(observer, topic, false)
322 module.onShutdown(->
323 Services.obs.removeObserver(observer, topic, false)
324 )
325
326 openPopup = (popup) ->
327 window = popup.ownerGlobal
328 # Show the popup so it gets a height and width.
329 popup.openPopupAtScreen(0, 0)
330 # Center the popup inside the window.
331 popup.moveTo(
332 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
333 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
334 )
335
336 openTab = (window, url, options) ->
337 {gBrowser} = window
338 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
339 gBrowser.loadOneTab(url, options)
340
341 writeToClipboard = (text) ->
342 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
343 .getService(Ci.nsIClipboardHelper)
344 clipboardHelper.copyString(text)
345
346
347
348 module.exports = {
349 isActivatable
350 isAdjustable
351 isContentEditable
352 isProperLink
353 isTextInputElement
354 isTypingElement
355
356 getActiveElement
357 blurActiveElement
358 blurActiveBrowserElement
359 focusElement
360 getFocusType
361
362 listen
363 listenOnce
364 onRemoved
365 suppressEvent
366 simulateClick
367
368 area
369 containsDeep
370 createBox
371 injectTemporaryPopup
372 insertText
373 querySelectorAllDeep
374 scroll
375 setAttributes
376
377 Counter
378 EventEmitter
379 has
380 nextTick
381 regexEscape
382 removeDuplicates
383 removeDuplicateCharacters
384
385 formatError
386 getCurrentLocation
387 getCurrentWindow
388 loadCss
389 observe
390 openPopup
391 openTab
392 writeToClipboard
393 }
Imprint / Impressum