# backtick_javascript: true # helpers: hash_put # Provides a complete set of tools to wrap native JavaScript # into nice Ruby objects. # # @example # # $$.document.querySelector('p').classList.add('blue') # # => adds "blue" class to

# # $$.location.href = 'https://google.com' # # => changes page location # # do_later = $$[:setTimeout] # Accessing the "setTimeout" property # do_later.call(->{ puts :hello}, 500) # # `$$` and `$global` wrap `Opal.global`, which the Opal JS runtime # sets to the global `this` object. # module Native def self.is_a?(object, klass) %x{ try { return #{object} instanceof #{try_convert(klass)}; } catch (e) { return false; } } end def self.try_convert(value, default = nil) %x{ if (#{native?(value)}) { return #{value}; } else if (#{value.respond_to? :to_n}) { return #{value.to_n}; } else { return #{default}; } } end def self.convert(value) %x{ if (#{native?(value)}) { return #{value}; } else if (#{value.respond_to? :to_n}) { return #{value.to_n}; } else { #{raise ArgumentError, "#{value.inspect} isn't native"}; } } end def self.call(obj, key, *args, &block) %x{ var prop = #{obj}[#{key}]; if (prop instanceof Function) { var converted = new Array(args.length); for (var i = 0, l = args.length; i < l; i++) { var item = args[i], conv = #{try_convert(`item`)}; converted[i] = conv === nil ? item : conv; } if (block !== nil) { converted.push(block); } return #{Native(`prop.apply(#{obj}, converted)`)}; } else { return #{Native(`prop`)}; } } end def self.proc(&block) raise LocalJumpError, 'no block given' unless block ::Kernel.proc { |*args| args.map! { |arg| Native(arg) } instance = Native(`this`) %x{ // if global is current scope, run the block in the scope it was defined if (this === Opal.global) { return block.apply(self, #{args}); } var self_ = block.$$s; block.$$s = null; try { return block.apply(#{instance}, #{args}); } finally { block.$$s = self_; } } } end module Helpers # Exposes a native JavaScript method to Ruby # # # @param new [String] # The name of the newly created method. # # @param old [String] # The name of the native JavaScript method to be exposed. # If the name ends with "=" (e.g. `foo=`) it will be interpreted as # a property setter. (default: the value of "new") # # @param as [Class] # If provided the values returned by the original method will be # returned as instances of the passed class. The class passed to "as" # is expected to accept a native JavaScript value. # # @example # # class Element # extend Native::Helpers # # alias_native :add_class, :addClass # alias_native :show # alias_native :hide # # def initialize(selector) # @native = `$(#{selector})` # end # end # # titles = Element.new('h1') # titles.add_class :foo # titles.hide # titles.show # def alias_native(new, old = new, as: nil) if old.end_with? '=' define_method new do |value| `#{@native}[#{old[0..-2]}] = #{Native.convert(value)}` value end elsif as define_method new do |*args, &block| value = Native.call(@native, old, *args, &block) if value as.new(value.to_n) end end else define_method new do |*args, &block| Native.call(@native, old, *args, &block) end end end def native_reader(*names) names.each do |name| define_method name do Native(`#{@native}[name]`) end end end def native_writer(*names) names.each do |name| define_method "#{name}=" do |value| Native(`#{@native}[name] = value`) end end end def native_accessor(*names) native_reader(*names) native_writer(*names) end end module Wrapper def initialize(native) unless ::Kernel.native?(native) ::Kernel.raise ArgumentError, "#{native.inspect} isn't native" end @native = native end # Returns the internal native JavaScript value def to_n @native end def self.included(klass) klass.extend Helpers end end def self.included(base) warn 'Including ::Native is deprecated. Please include Native::Wrapper instead.' base.include Wrapper end end module Kernel def native?(value) `value == null || !value.$$class` end # Wraps a native JavaScript with `Native::Object.new` # # @return [Native::Object] The wrapped object if it is native # @return [nil] for `null` and `undefined` # @return [obj] The object itself if it's not native def Native(obj) if `#{obj} == null` nil elsif native?(obj) Native::Object.new(obj) elsif obj.is_a?(Array) obj.map do |o| Native(o) end elsif obj.is_a?(Proc) proc do |*args, &block| Native(obj.call(*args, &block)) end else obj end end alias _Array Array # Wraps array-like JavaScript objects in Native::Array def Array(object, *args, &block) if native?(object) return Native::Array.new(object, *args, &block).to_a end _Array(object) end end class Native::Object < BasicObject include ::Native::Wrapper def ==(other) `#{@native} === #{::Native.try_convert(other)}` end def has_key?(name) `Opal.hasOwnProperty.call(#{@native}, #{name})` end def each(*args) if block_given? %x{ for (var key in #{@native}) { #{yield `key`, `#{@native}[key]`} } } self else method_missing(:each, *args) end end def [](key) %x{ var prop = #{@native}[key]; if (prop instanceof Function) { return prop; } else { return #{::Native.call(@native, key)} } } end def []=(key, value) native = ::Native.try_convert(value) if `#{native} === nil` `#{@native}[key] = #{value}` else `#{@native}[key] = #{native}` end end def merge!(other) %x{ other = #{::Native.convert(other)}; for (var prop in other) { #{@native}[prop] = other[prop]; } } self end def respond_to?(name, include_all = false) ::Kernel.instance_method(:respond_to?).bind(self).call(name, include_all) end def respond_to_missing?(name, include_all = false) `Opal.hasOwnProperty.call(#{@native}, #{name})` end def method_missing(mid, *args, &block) %x{ if (mid.charAt(mid.length - 1) === '=') { return #{self[mid.slice(0, mid.length - 1)] = args[0]}; } else { return #{::Native.call(@native, mid, *args, &block)}; } } end def nil? false end def is_a?(klass) `Opal.is_a(self, klass)` end def instance_of?(klass) `self.$$class === klass` end def class `self.$$class` end def to_a(options = {}, &block) ::Native::Array.new(@native, options, &block).to_a end def inspect "#" end alias include? has_key? alias key? has_key? alias kind_of? is_a? alias member? has_key? end class Native::Array include Native::Wrapper include Enumerable def initialize(native, options = {}, &block) super(native) @get = options[:get] || options[:access] @named = options[:named] @set = options[:set] || options[:access] @length = options[:length] || :length @block = block if `#{length} == null` raise ArgumentError, 'no length found on the array-like object' end end def each(&block) return enum_for :each unless block %x{ for (var i = 0, length = #{length}; i < length; i++) { Opal.yield1(block, #{self[`i`]}); } } self end def [](index) result = case index when String, Symbol @named ? `#{@native}[#{@named}](#{index})` : `#{@native}[#{index}]` when Integer @get ? `#{@native}[#{@get}](#{index})` : `#{@native}[#{index}]` end if result if @block @block.call(result) else Native(result) end end end def []=(index, value) if @set `#{@native}[#{@set}](#{index}, #{Native.convert(value)})` else `#{@native}[#{index}] = #{Native.convert(value)}` end end def last(count = nil) if count index = length - 1 result = [] while index >= 0 result << self[index] index -= 1 end result else self[length - 1] end end def length `#{@native}[#{@length}]` end def inspect to_a.inspect end alias to_ary to_a end class Numeric # @return the internal JavaScript value (with `valueOf`). def to_n `self.valueOf()` end end class Proc # @return itself (an instance of `Function`) def to_n self end end class String # @return the internal JavaScript value (with `valueOf`). def to_n `self.valueOf()` end end class Regexp # @return the internal JavaScript value (with `valueOf`). def to_n `self.valueOf()` end end class MatchData # @return the array of matches def to_n @matches end end class Struct # @return a JavaScript object with the members as keys and their # values as values. def to_n result = `{}` each_pair do |name, value| `#{result}[#{name}] = #{Native.try_convert(value, value)}` end result end end class Array # Retuns a copy of itself trying to call #to_n on each member. def to_n %x{ var result = []; for (var i = 0, length = self.length; i < length; i++) { var obj = self[i]; result.push(#{Native.try_convert(`obj`, `obj`)}); } return result; } end end class Boolean # @return the internal JavaScript value (with `valueOf`). def to_n `self.valueOf()` end end class Time # @return itself (an instance of `Date`). def to_n self end end class NilClass # @return the corresponding JavaScript value (`null`). def to_n `null` end end # Running this code twice results in an infinite loop. While it's true # that we shouldn't run this file twice, there are certain cases, like # for example live reload, when this may happen. unless Hash.method_defined? :_initialize class Hash alias _initialize initialize %x{ function $hash_convert_and_put_value(hash, key, value) { if (value && (value.constructor === undefined || value.constructor === Object || value instanceof Map)) { $hash_put(hash, key, #{Hash.new(`value`)}); } else if (value && value.$$is_array) { value = value.map(function(item) { if (item && (item.constructor === undefined || item.constructor === Object || value instanceof Map)) { return #{Hash.new(`item`)}; } return #{Native(`item`)}; }); $hash_put(hash, key, value) } else { $hash_put(hash, key, #{Native(`value`)}); } } } def initialize(defaults = undefined, &block) %x{ if (defaults != null) { if (defaults.constructor === undefined || defaults.constructor === Object) { var key, value; for (key in defaults) { value = defaults[key]; $hash_convert_and_put_value(self, key, value); } return self; } else if (defaults instanceof Map) { Opal.hash_each(defaults, false, function(key, value) { $hash_convert_and_put_value(self, key, value); return [false, false]; }); } } return #{_initialize(defaults, &block)}; } end # @return a JavaScript object, in turn also calling #to_n on # all keys and values. def to_n %x{ var result = {}; Opal.hash_each(self, false, function(key, value) { result[#{Native.try_convert(`key`, `key`)}] = #{Native.try_convert(`value`, `value`)}; return [false, false]; }); return result; } end end end class Module # Exposes the current module as a property of # the global object (e.g. `window`). def native_module `Opal.global[#{name}] = #{self}` end end class Class def native_alias(new_jsid, existing_mid) %x{ var aliased = #{self}.prototype[Opal.jsid(#{existing_mid})]; if (!aliased) { #{raise NameError.new("undefined method `#{existing_mid}' for class `#{inspect}'", existing_mid)}; } #{self}.prototype[#{new_jsid}] = aliased; } end def native_class native_module `self["new"] = self.$new` end end # Exposes the global value (would be `window` inside a browser) $$ = $global = Native(`Opal.global`)