# 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
"#