# backtick_javascript: true module Browser class Event include Native::Wrapper # @see https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events class Definition include Native::Wrapper # @private def self.new(&block) data = super(`{ bubbles: true, cancelable: true }`) block.call(data) if block data.to_n end # Set the event as bubbling. alias_native :bubbles= # Set the event as cancelable. alias_native :cancelable= end module Target # @private def self.converters @converters ||= [] end # @private def self.register(&block) converters << block end # @private def self.convert(value) return value unless native?(value) converters.each {|block| if result = block.call(value) return result end } nil end def self.included(klass) klass.instance_eval { def self.target(&block) Event::Target.register(&block) end } end class Callback attr_reader :target, :name, :selector # @private def initialize(target, name, selector = nil, &block) @target = target @name = name @selector = selector @block = block end # Call the callback with the given event. # # @param event [native] the native event object def call(event) to_proc.call(event) end # Get the native function linked to the callback. def to_proc @proc ||= -> event { %x{ if (!event.currentTarget) { event.currentTarget = self.target.native; } } event = Event.new(event, self) unless event.stopped? @block.call(event, *event.arguments) end !event.prevented? } end # @!attribute [r] event # @return [Class] the class for the event def event Event.class_for(@name) end # Stop listening for the event linked to the callback. def off target.off(self) end end class Delegate def initialize(target, name, pair) @target = target @name = name @pair = pair end # Stop listening for the event linked to the delegate. def off delegate = @target.delegated[@name] delegate.last.delete(@pair) if delegate.last.empty? delegate.first.off delegate.delete(@name) end end end Delegates = Struct.new(:callback, :handlers) # @overload on(name, &block) # # Start listening for an event on the target. # # @param name [String] the event name # # @yieldparam event [Event] the event # # @return [Callback] # # @overload on(name, selector, &block) # # Start listening for an event on the target children. # # @param name [String] the event name # @param selector [String] the CSS selector to trigger the event on # # @yieldparam event [Event] the event # # @return [Delegate] def on(name, selector = nil, &block) raise ArgumentError, 'no block has been given' unless block name = Event.name_for(name) if selector unless delegate = delegated[name] delegate = delegated[name] = Delegates.new if %w[blur focus].include?(name) delegate.callback = on! name do |e| delegate(delegate, e) end else delegate.callback = on name do |e| delegate(delegate, e) end end pair = [selector, block] delegate.handlers = [pair] Delegate.new(self, name, pair) else pair = [selector, block] delegate.handlers << pair Delegate.new(self, name, pair) end else callback = Callback.new(self, name, selector, &block) callbacks.push(callback) attach(callback) end end # Start listening for an event in the capturing phase. # # @param name [String] the event name # # @yieldparam event [Event] the event # # @return [Callback] def on!(name, &block) raise ArgumentError, 'no block has been given' unless block name = Event.name_for(name) callback = Callback.new(self, name, &block) callbacks.push(callback) attach!(callback) end if Browser.supports? 'Event.addListener' def attach(callback) `#@native.addEventListener(#{callback.name}, #{callback.to_proc})` callback end def attach!(callback) `#@native.addEventListener(#{callback.name}, #{callback.to_proc}, true)` callback end elsif Browser.supports? 'Event.attach' def attach(callback) if callback.event == Custom %x{ if (!#@native.$custom) { #@native.$custom = function(event) { for (var i = 0, length = #@native.$callbacks.length; i < length; i++) { var callback = #@native.$callbacks[i]; if (#{`callback`.event == Custom}) { event.type = callback.name; #{`callback`.call(`event`)}; } } }; #@native.attachEvent("ondataavailable", #@native.$custom); } } else `#@native.attachEvent("on" + #{callback.name}, #{callback.to_proc})` end callback end def attach!(callback) case callback.name when :blur `#@native.attachEvent("onfocusout", #{callback.to_proc})` when :focus `#@native.attachEvent("onfocusin", #{callback.to_proc})` else warn "attach: capture doesn't work on this browser" attach(callback) end callback end else # @todo implement polyfill # @private def attach(*) raise NotImplementedError end # @todo implement polyfill # @private def attach!(*) raise NotImplementedError end end # @overload one(name, &block) # # Start listening for an event on the target. Remove the event after firing # so that it is fired at most once. # # @param name [String] the event name # # @yieldparam event [Event] the event # # @return [Callback] # # @overload one(name, selector, &block) # # Start listening for an event on the target children. Remove the event after # firing so that it is fired at most once. # # @param name [String] the event name # @param selector [String] the CSS selector to trigger the event on # # @yieldparam event [Event] the event # # @return [Delegate] def one (name, selector = nil, &block) raise ArgumentError, 'no block has been given' unless block cb = on name, selector do |*args| out = block.call(*args) cb.off out end end # @overload off() # Stop listening for any event. # # @overload off(what) # Stop listening for an event. # # @param what [Callback, String, Regexp] what to stop listening for def off(what = nil) case what when Callback callbacks.delete(what) detach(what) when String if what.include?(?*) or what.include?(??) off(Regexp.new(what.gsub(/\*/, '.*?').gsub(/\?/, ?.))) else what = Event.name_for(what) callbacks.delete_if {|callback| if callback.name == what detach(callback) true end } end when Regexp callbacks.delete_if {|callback| if callback.name =~ what detach(callback) true end } else callbacks.each {|callback| detach(callback) } callbacks.clear end end if Browser.supports? 'Event.removeListener' def detach(callback) `#@native.removeEventListener(#{callback.name}, #{callback.to_proc}, false)` end elsif Browser.supports? 'Event.detach' def detach(callback) if callback.event == Custom if callbacks.none? { |c| c.event == Custom } %x{ #@native.detachEvent("ondataavailable", #@native.$custom); delete #@native.$custom; } end else `#@native.detachEvent("on" + #{callback.name}, #{callback.to_proc})` end end else # @todo implement internal handler thing # @private def detach(callback) raise NotImplementedError end end # Trigger an event on the target. # # @param event [String] the event name # @param args [Array] optional arguments to the event callback # # @yieldparam definition [Definition] definition to customize the event def trigger(event, *args, &block) if event.is_a? String event = Event.create(event, *args, &block) end dispatch(event) end # Trigger an event on the target without bubbling. # # @param event [String] the event name # @param args [Array] optional arguments to the event callback # # @yieldparam definition [Definition] definition to customize the event def trigger!(event, *args, &block) trigger event, *args do |e| block.call(e) if block e.bubbles = false end end if Browser.supports? 'Event.dispatch' def dispatch(event) `#@native.dispatchEvent(#{event.to_n})` end elsif Browser.supports? 'Event.fire' def dispatch(event) if Custom === event `#@native.fireEvent("ondataavailable", #{event.to_n})` else `#@native.fireEvent("on" + #{event.name}, #{event.to_n})` end end else # @todo implement polyfill # @private def dispatch(*) raise NotImplementedError end end private def callbacks %x{ if (!#@native.$callbacks) { #@native.$callbacks = []; } return #@native.$callbacks; } end def delegated %x{ if (!#@native.$delegated) { #@native.$delegated = #{{}}; } return #@native.$delegated; } end def delegate(delegates, event, element = event.target) return if element.nil? || element == event.on delegates.handlers.each {|selector, block| if element =~ selector new = event.dup new.on = element block.call new, *new.arguments end } delegate(delegates, event, element.parent) end end end end