opal/opal-browser

View on GitHub
opal/browser/event/base.rb

Summary

Maintainability
C
1 day
Test Coverage
# 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