Merge pull request #3611 from atom/ns-activation-commands

Activation commands
This commit is contained in:
Nathan Sobo
2014-09-24 15:06:52 -06:00
12 changed files with 775 additions and 647 deletions

View File

@@ -46,13 +46,15 @@ class CommandRegistry
constructor: (@rootNode) ->
@listenersByCommandName = {}
getRootNode: -> @rootNode
setRootNode: (newRootNode) ->
oldRootNode = @rootNode
@rootNode = newRootNode
for commandName of @listenersByCommandName
oldRootNode?.removeEventListener(commandName, @dispatchCommand, true)
newRootNode?.addEventListener(commandName, @dispatchCommand, true)
@removeCommandListener(oldRootNode, commandName)
@addCommandListener(newRootNode, commandName)
# Public: Add one or more command listeners associated with a selector.
#
@@ -88,7 +90,7 @@ class CommandRegistry
return disposable
unless @listenersByCommandName[commandName]?
@rootNode?.addEventListener(commandName, @dispatchCommand, true)
@addCommandListener(@rootNode, commandName)
@listenersByCommandName[commandName] = []
listener = new CommandListener(selector, callback)
@@ -99,35 +101,7 @@ class CommandRegistry
listenersForCommand.splice(listenersForCommand.indexOf(listener), 1)
if listenersForCommand.length is 0
delete @listenersByCommandName[commandName]
@rootNode.removeEventListener(commandName, @dispatchCommand, true)
dispatchCommand: (event) =>
propagationStopped = false
immediatePropagationStopped = false
currentTarget = event.target
syntheticEvent = Object.create event,
eventPhase: value: Event.BUBBLING_PHASE
currentTarget: get: -> currentTarget
stopPropagation: value: ->
propagationStopped = true
stopImmediatePropagation: value: ->
propagationStopped = true
immediatePropagationStopped = true
loop
matchingListeners =
@listenersByCommandName[event.type]
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
for listener in matchingListeners
break if immediatePropagationStopped
listener.callback.call(currentTarget, syntheticEvent)
break if propagationStopped
break if currentTarget is @rootNode
currentTarget = currentTarget.parentNode
@removeCommandListener(@rootNode, commandName)
# Public: Find all registered commands matching a query.
#
@@ -163,10 +137,86 @@ class CommandRegistry
commands
clear: ->
@listenersByCommandName = {}
# Public: Simulate the dispatch of a command on a DOM node.
#
# This can be useful for testing when you want to simulate the invocation of a
# command on a detached DOM node. Otherwise, the DOM node in question needs to
# be attached to the document so the event bubbles up to the root node to be
# processed.
#
# * `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)
@handleCommandEvent(eventWithTarget)
getSnapshot: ->
_.deepClone(@listenersByCommandName)
restoreSnapshot: (snapshot) ->
rootNode = @getRootNode()
@setRootNode(null) # clear listeners for current commands
@listenersByCommandName = _.deepClone(snapshot)
@setRootNode(rootNode) # restore listeners for commands in snapshot
handleCommandEvent: (originalEvent) =>
originalEvent.__handledByCommandRegistry = true
propagationStopped = false
immediatePropagationStopped = false
matched = false
currentTarget = originalEvent.target
invokedListeners = []
syntheticEvent = Object.create originalEvent,
eventPhase: value: Event.BUBBLING_PHASE
currentTarget: get: -> currentTarget
stopPropagation: value: ->
originalEvent.stopPropagation()
propagationStopped = true
stopImmediatePropagation: value: ->
originalEvent.stopImmediatePropagation()
propagationStopped = true
immediatePropagationStopped = true
disableInvokedListeners: value: ->
listener.enabled = false for listener in invokedListeners
-> listener.enabled = true for listener in invokedListeners
loop
matchingListeners =
(@listenersByCommandName[originalEvent.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
matched = true if matchingListeners.length > 0
for listener in matchingListeners when listener.enabled
break if immediatePropagationStopped
invokedListeners.push(listener)
listener.callback.call(currentTarget, syntheticEvent)
break unless currentTarget?
break if currentTarget is @rootNode
break if propagationStopped
currentTarget = currentTarget.parentNode
matched
handleJQueryCommandEvent: (event) =>
@handleCommandEvent(event) unless event.originalEvent?.__handledByCommandRegistry
addCommandListener: (node, commandName, listener) ->
node?.addEventListener(commandName, @handleCommandEvent, true)
$(node).on commandName, @handleJQueryCommandEvent
removeCommandListener: (node, commandName) ->
node?.removeEventListener(commandName, @handleCommandEvent, true)
$(node).off commandName, @handleJQueryCommandEvent
class CommandListener
enabled: true
constructor: (@selector, @callback) ->
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))
@sequenceNumber = SequenceCount++

View File

@@ -5,7 +5,7 @@ async = require 'async'
CSON = require 'season'
fs = require 'fs-plus'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
{Emitter, CompositeDisposable} = require 'event-kit'
Q = require 'q'
{deprecate} = require 'grim'
@@ -99,7 +99,7 @@ class Package
@loadMenus()
@loadStylesheets()
@scopedPropertiesPromise = @loadScopedProperties()
@requireMainModule() unless @hasActivationEvents()
@requireMainModule() unless @hasActivationCommands()
catch error
console.warn "Failed to load package named '#{@name}'", error.stack ? error
@@ -119,8 +119,8 @@ class Package
@activationDeferred = Q.defer()
@measure 'activateTime', =>
@activateResources()
if @hasActivationEvents()
@subscribeToActivationEvents()
if @hasActivationCommands()
@subscribeToActivationCommands()
else
@activateNow()
@@ -270,7 +270,7 @@ class Package
deactivate: ->
@activationDeferred?.reject()
@activationDeferred = null
@unsubscribeFromActivationEvents()
@activationCommandSubscriptions?.dispose()
@deactivateResources()
@deactivateConfig()
if @mainActivated
@@ -319,64 +319,56 @@ class Package
path.join(@path, 'index')
@mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...])
hasActivationEvents: ->
if _.isArray(@metadata.activationEvents)
return @metadata.activationEvents.some (activationEvent) ->
activationEvent?.length > 0
else if _.isString(@metadata.activationEvents)
return @metadata.activationEvents.length > 0
else if _.isObject(@metadata.activationEvents)
for event, selector of @metadata.activationEvents
return true if event.length > 0 and selector.length > 0
hasActivationCommands: ->
for selector, commands of @getActivationCommands()
return true if commands.length > 0
false
subscribeToActivationEvents: ->
return unless @metadata.activationEvents?
if _.isArray(@metadata.activationEvents)
atom.workspaceView.command(event, @handleActivationEvent) for event in @metadata.activationEvents
else if _.isString(@metadata.activationEvents)
atom.workspaceView.command(@metadata.activationEvents, @handleActivationEvent)
else
atom.workspaceView.command(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents
subscribeToActivationCommands: ->
@activationCommandSubscriptions = new CompositeDisposable
for selector, commands of @getActivationCommands()
for command in commands
@activationCommandSubscriptions.add(
atom.commands.add(selector, command, @handleActivationCommand)
)
handleActivationEvent: (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
getActivationCommands: ->
return @activationCommands if @activationCommands?
@activationCommands = {}
if @metadata.activationCommands?
for selector, commands of @metadata.activationCommands
@activationCommands[selector] ?= []
if _.isString(commands)
@activationCommands[selector].push(commands)
else if _.isArray(commands)
@activationCommands[selector].push(commands...)
if @metadata.activationEvents?
if _.isArray(@metadata.activationEvents)
for eventName in @metadata.activationEvents
@activationCommands['.workspace'] ?= []
@activationCommands['.workspace'].push(eventName)
else if _.isString(@metadata.activationEvents)
eventName = @metadata.activationEvents
@activationCommands['.workspace'] ?= []
@activationCommands['.workspace'].push(eventName)
else
for eventName, selector of @metadata.activationEvents
selector ?= '.workspace'
@activationCommands[selector] ?= []
@activationCommands[selector].push(eventName)
@activationCommands
handleActivationCommand: (event) =>
event.stopImmediatePropagation()
@activationCommandSubscriptions.dispose()
reenableInvokedListeners = event.disableInvokedListeners()
@activateNow()
$ ?= require('./space-pen-extensions').$
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents()
false
unsubscribeFromActivationEvents: ->
return unless atom.workspaceView?
if _.isArray(@metadata.activationEvents)
atom.workspaceView.off(event, @handleActivationEvent) for event in @metadata.activationEvents
else if _.isString(@metadata.activationEvents)
atom.workspaceView.off(@metadata.activationEvents, @handleActivationEvent)
else
atom.workspaceView.off(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
$ ?= require('./space-pen-extensions').$
element = $(event.target)
while element.length
if eventHandlers = element.handlers()?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler
event.target.dispatchEvent(new CustomEvent(event.type, bubbles: true))
reenableInvokedListeners()
# Does the given module path contain native code?
isNativeModule: (modulePath) ->