# Public: Provides a complete set of tools to wrap native JavaScript # into nice Ruby objects. # # Examples # # $$.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)
#
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)
%x{
if (#{native?(value)}) {
return #{value};
}
else if (#{value.respond_to? :to_n}) {
return #{value.to_n};
}
else {
return nil;
}
}
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, length = args.length; i < length; 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
# Public: Exposes a native JavaScript method to Ruby
#
#
# new - The name of the newly created method.
# old - 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")
# as - 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.
#
# Examples
#
# class Element
# include 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
else
if as
define_method new do |*args, &block|
if value = Native.call(@native, old, *args, &block)
as.new(value.to_n)
end
end
else
define_method new do |*args, &block|
Native.call(@native, old, *args, &block)
end
end
end
end
def native_reader(*names)
names.each {|name|
define_method name do
Native(`#@native[name]`)
end
}
end
def native_writer(*names)
names.each {|name|
define_method "#{name}=" do |value|
Native(`#@native[name] = value`)
end
}
end
def native_accessor(*names)
native_reader(*names)
native_writer(*names)
end
end
def self.included(klass)
klass.extend Helpers
end
def initialize(native)
unless Kernel.native?(native)
Kernel.raise ArgumentError, "#{native.inspect} isn't native"
end
@native = native
end
# Public: Returns the internal native JavaScript value
def to_n
@native
end
end
module Kernel
def native?(value)
`value == null || !value.$$class`
end
# Public: Wraps a native JavaScript with Native::Object.new
#
# Returns:
# 1. The wrapped object if it is native
# 2. nil for `null` and `undefined`
# 3. 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_method :_Array, :Array
# Public: 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
return _Array(object)
end
end
class Native::Object < BasicObject
include ::Native
def ==(other)
`#@native === #{Native.try_convert(other)}`
end
def has_key?(name)
`Opal.hasOwnProperty.call(#@native, #{name})`
end
alias key? has_key?
alias include? has_key?
alias member? has_key?
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
alias kind_of? is_a?
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
"#