Integrate jQuery::on and ::trigger with command registry dispatch

This commit is contained in:
Nathan Sobo
2014-10-06 15:56:30 -06:00
parent 0d55a377fb
commit cdb4ed1327
8 changed files with 120 additions and 74 deletions

View File

@@ -55,7 +55,7 @@
"season": "^1.0.2",
"semver": "1.1.4",
"serializable": "^1",
"space-pen": "3.6.1",
"space-pen": "3.7.0",
"temp": "0.7.0",
"text-buffer": "^3.2.8",
"theorist": "^1.0.2",

View File

@@ -14,7 +14,10 @@ describe "CommandRegistry", ->
parent.appendChild(child)
document.querySelector('#jasmine-content').appendChild(parent)
registry = new CommandRegistry(parent)
registry = new CommandRegistry
afterEach ->
registry.destroy()
describe "when a command event is dispatched on an element", ->
it "invokes callbacks with selectors matching the target", ->
@@ -105,6 +108,16 @@ describe "CommandRegistry", ->
grandchild.dispatchEvent(dispatchedEvent)
expect(dispatchedEvent.preventDefault).toHaveBeenCalled()
it "forwards .abortKeyBinding() calls from the synthetic event to the original", ->
calls = []
registry.add '.child', 'command', (event) -> event.abortKeyBinding()
dispatchedEvent = new CustomEvent('command', bubbles: true)
dispatchedEvent.abortKeyBinding = jasmine.createSpy('abortKeyBinding')
grandchild.dispatchEvent(dispatchedEvent)
expect(dispatchedEvent.abortKeyBinding).toHaveBeenCalled()
it "allows listeners to be removed via a disposable returned by ::add", ->
calls = []

View File

@@ -72,7 +72,6 @@ beforeEach ->
atom.project = new Project(paths: [projectPath])
atom.workspace = new Workspace()
atom.keymaps.keyBindings = _.clone(keyBindingsToRestore)
atom.commands.setRootNode(document.body)
atom.commands.restoreSnapshot(commandsToRestore)
window.resetTimeouts()

View File

@@ -47,14 +47,6 @@ describe "Window", ->
$(window).trigger 'window:close'
expect(atom.close).toHaveBeenCalled()
it "emits the beforeunload event", ->
$(window).off 'beforeunload'
beforeunload = jasmine.createSpy('beforeunload').andReturn(false)
$(window).on 'beforeunload', beforeunload
$(window).trigger 'window:close'
expect(beforeunload).toHaveBeenCalled()
describe "beforeunload event", ->
[beforeUnloadEvent] = []

View File

@@ -1,4 +1,4 @@
{Disposable, CompositeDisposable} = require 'event-kit'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
{specificity} = require 'clear-cut'
_ = require 'underscore-plus'
{$} = require './space-pen-extensions'
@@ -47,15 +47,11 @@ class CommandRegistry
@registeredCommands = {}
@selectorBasedListenersByCommandName = {}
@inlineListenersByCommandName = {}
@emitter = new Emitter
getRootNode: -> @rootNode
setRootNode: (newRootNode) ->
oldRootNode = @rootNode
@rootNode = newRootNode
destroy: ->
for commandName of @registeredCommands
@removeDOMListener(oldRootNode, commandName)
@addDOMListener(newRootNode, commandName)
window.removeEventListener(commandName, @handleCommandEvent, true)
# Public: Add one or more command listeners associated with a selector.
#
@@ -122,7 +118,6 @@ class CommandRegistry
new Disposable =>
listenersForElement.splice(listenersForElement.indexOf(listener), 1)
listenersForCommand.delete(element) if listenersForElement.length is 0
@listenerRemovedForCommand(commandName)
# Public: Find all registered commands matching a query.
#
@@ -137,12 +132,11 @@ class CommandRegistry
# `$::command` method.
findCommands: ({target}) ->
commands = []
target = @rootNode unless @rootNode.contains(target)
currentTarget = target
loop
for commandName, listeners of @selectorBasedListenersByCommandName
for listener in listeners
if currentTarget.webkitMatchesSelector(listener.selector)
if currentTarget.webkitMatchesSelector?(listener.selector)
commands.push
name: commandName
displayName: _.humanizeEventName(commandName)
@@ -168,11 +162,18 @@ class CommandRegistry
#
# * `target` The DOM node at which to start bubbling the command event.
# * `commandName` {String} indicating the name of the command to dispatch.
dispatch: (target, commandName) ->
event = new CustomEvent(commandName, bubbles: true)
eventWithTarget = Object.create(event, target: value: target)
dispatch: (target, commandName, detail) ->
event = new CustomEvent(commandName, {bubbles: true, detail})
eventWithTarget = Object.create event,
target: value: target
preventDefault: value: ->
stopPropagation: value: ->
stopImmediatePropagation: value: ->
@handleCommandEvent(eventWithTarget)
onWillDispatch: (callback) ->
@emitter.on 'will-dispatch', callback
getSnapshot: ->
snapshot = {}
for commandName, listeners of @selectorBasedListenersByCommandName
@@ -180,15 +181,9 @@ class CommandRegistry
snapshot
restoreSnapshot: (snapshot) ->
rootNode = @getRootNode()
@setRootNode(null) # clear listeners for current commands
@registeredCommands = {}
@inlineListenersByCommandName = {}
@selectorBasedListenersByCommandName = {}
for commandName, listeners of snapshot
@selectorBasedListenersByCommandName[commandName] = listeners.slice()
@registeredCommands[commandName] = true
@setRootNode(rootNode) # restore listeners for commands in snapshot
handleCommandEvent: (originalEvent) =>
originalEvent.__handledByCommandRegistry = true
@@ -197,7 +192,6 @@ class CommandRegistry
immediatePropagationStopped = false
matched = false
currentTarget = originalEvent.target
invokedListeners = []
syntheticEvent = Object.create originalEvent,
eventPhase: value: Event.BUBBLING_PHASE
@@ -209,50 +203,38 @@ class CommandRegistry
originalEvent.stopImmediatePropagation()
propagationStopped = true
immediatePropagationStopped = true
disableInvokedListeners: value: ->
listener.enabled = false for listener in invokedListeners
-> listener.enabled = true for listener in invokedListeners
abortKeyBinding: value: ->
originalEvent.abortKeyBinding?()
@emitter.emit 'will-dispatch', syntheticEvent
loop
inlineListeners = @inlineListenersByCommandName[originalEvent.type]?.get(currentTarget) ? []
selectorBasedListeners =
(@selectorBasedListenersByCommandName[originalEvent.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
listeners = inlineListeners.concat(selectorBasedListeners)
listeners = @inlineListenersByCommandName[originalEvent.type]?.get(currentTarget) ? []
unless currentTarget is window or currentTarget is document
selectorBasedListeners =
(@selectorBasedListenersByCommandName[originalEvent.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
listeners = listeners.concat(selectorBasedListeners)
matched = true if listeners.length > 0
for listener in listeners when listener.enabled
for listener in listeners
break if immediatePropagationStopped
invokedListeners.push(listener)
listener.callback.call(currentTarget, syntheticEvent)
break if currentTarget is @rootNode
break if currentTarget is window
break if propagationStopped
currentTarget = currentTarget.parentNode
break unless currentTarget?
currentTarget = currentTarget.parentNode ? window
matched
handleJQueryCommandEvent: (event) =>
@handleCommandEvent(event) unless event.originalEvent?.__handledByCommandRegistry
commandRegistered: (commandName) ->
unless @registeredCommands[commandName]
@addDOMListener(@rootNode, commandName)
window.addEventListener(commandName, @handleCommandEvent, true)
@registeredCommands[commandName] = true
addDOMListener: (node, commandName) ->
node?.addEventListener(commandName, @handleCommandEvent, true)
$(node).on commandName, @handleJQueryCommandEvent
removeDOMListener: (node, commandName) ->
node?.removeEventListener(commandName, @handleCommandEvent, true)
$(node).off commandName, @handleJQueryCommandEvent
class SelectorBasedListener
enabled: true
constructor: (@selector, @callback) ->
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))
@sequenceNumber = SequenceCount++
@@ -262,6 +244,4 @@ class SelectorBasedListener
other.sequenceNumber - @sequenceNumber
class InlineListener
enabled: true
constructor: (@callback) ->

View File

@@ -334,9 +334,17 @@ class Package
@activationCommandSubscriptions = new CompositeDisposable
for selector, commands of @getActivationCommands()
for command in commands
@activationCommandSubscriptions.add(
atom.commands.add(selector, command, @handleActivationCommand)
)
do (selector, command) =>
@activationCommandSubscriptions.add(atom.commands.onWillDispatch (event) =>
return unless event.type is command
currentTarget = event.target
while currentTarget
if currentTarget.webkitMatchesSelector(selector)
@activationCommandSubscriptions.dispose()
@activateNow()
break
currentTarget = currentTarget.parentElement
)
getActivationCommands: ->
return @activationCommands if @activationCommands?
@@ -368,14 +376,6 @@ class Package
@activationCommands
handleActivationCommand: (event) =>
event.stopImmediatePropagation()
@activationCommandSubscriptions.dispose()
reenableInvokedListeners = event.disableInvokedListeners()
@activateNow()
event.target.dispatchEvent(new CustomEvent(event.type, bubbles: true))
reenableInvokedListeners()
# Does the given module path contain native code?
isNativeModule: (modulePath) ->
try

View File

@@ -10,6 +10,69 @@ jQuery.cleanData = (elements) ->
jQuery(element).view()?.unsubscribe() for element in elements
originalCleanData(elements)
NativeEventNames = new Set
NativeEventNames.add(nativeEvent) for nativeEvent in ["blur", "focus", "focusin",
"focusout", "load", "resize", "scroll", "unload", "click", "dblclick", "mousedown",
"mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "change",
"select", "submit", "keydown", "keypress", "keyup", "error", "contextmenu"]
originalTrigger = jQuery.fn.trigger
jQuery.fn.trigger = (eventName, data) ->
if NativeEventNames.has(eventName) or typeof eventName is 'object'
originalTrigger.call(this, eventName, data)
else
for element in this
atom.commands.dispatch(element, eventName, data)
this
HandlersByOriginalHandler = new WeakMap
CommandDisposablesByElement = new WeakMap
AddEventListener = (element, type, listener) ->
if NativeEventNames.has(type)
element.addEventListener(type, listener)
else
disposable = atom.commands.add(element, type, listener)
unless disposablesByType = CommandDisposablesByElement.get(element)
disposablesByType = {}
CommandDisposablesByElement.set(element, disposablesByType)
unless disposablesByListener = disposablesByType[type]
disposablesByListener = new WeakMap
disposablesByType[type] = disposablesByListener
disposablesByListener.set(listener, disposable)
RemoveEventListener = (element, type, listener) ->
if NativeEventNames.has(type)
element.removeEventListener(type, listener)
else
CommandDisposablesByElement.get(element)?[type]?.get(listener)?.dispose()
JQueryEventAdd = jQuery.event.add
jQuery.event.add = (elem, types, originalHandler, data, selector) ->
handler = (event) ->
if arguments.length is 1 and event.originalEvent?.detail?
{detail} = event.originalEvent
if Array.isArray(detail)
originalHandler.apply(this, [event].concat(detail))
else
originalHandler.call(this, event, detail)
else
originalHandler.apply(this, arguments)
HandlersByOriginalHandler.set(originalHandler, handler)
console.log "jquery.event.add...", elem, types if global.debug
JQueryEventAdd.call(this, elem, types, handler, data, selector, AddEventListener if atom?.commands?)
JQueryEventRemove = jQuery.event.remove
jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) ->
if originalHandler?
handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler
JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?)
tooltipDefaults =
delay:
show: 1000

View File

@@ -10,7 +10,6 @@ module.exports =
class WorkspaceElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
atom.commands.setRootNode(this)
@initializeContent()
@observeScrollbarStyle()
@observeTextEditorFontConfig()