From cdb4ed1327808d8068952927eb7c92f922848d1c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 6 Oct 2014 15:56:30 -0600 Subject: [PATCH] Integrate jQuery::on and ::trigger with command registry dispatch --- package.json | 2 +- spec/command-registry-spec.coffee | 15 +++++- spec/spec-helper.coffee | 1 - spec/window-spec.coffee | 8 --- src/command-registry.coffee | 82 ++++++++++++------------------- src/package.coffee | 22 ++++----- src/space-pen-extensions.coffee | 63 ++++++++++++++++++++++++ src/workspace-element.coffee | 1 - 8 files changed, 120 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 8fe911546..62f2fb390 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index ae01d29c8..ef2147dd0 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -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 = [] diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 7f99d9c2b..e483d25a5 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -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() diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index b5bf3ddc7..9b0c01007 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -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] = [] diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 5a7590edb..bfda3a5f5 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -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) -> diff --git a/src/package.coffee b/src/package.coffee index 143629d38..12a0c5767 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -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 diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee index 14bf076f1..593b9cea9 100644 --- a/src/space-pen-extensions.coffee +++ b/src/space-pen-extensions.coffee @@ -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 diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 3585491ca..06cbca188 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -10,7 +10,6 @@ module.exports = class WorkspaceElement extends HTMLElement createdCallback: -> @subscriptions = new CompositeDisposable - atom.commands.setRootNode(this) @initializeContent() @observeScrollbarStyle() @observeTextEditorFontConfig()