Skip to content

Commit

Permalink
Shadow DOM implementation.
Browse files Browse the repository at this point in the history
Notifying issues: opal#46, opal#82
  • Loading branch information
hmdne committed Jul 5, 2020
1 parent 85ccf1a commit e48c3c7
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 12 deletions.
2 changes: 1 addition & 1 deletion opal/browser/css/style_sheet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class StyleSheet
include Browser::NativeCachedWrapper

def initialize(what)
if what.is_a? DOM::Element
if DOM::Element === what
super(`#{what.to_n}.sheet`)
else
super(what)
Expand Down
2 changes: 2 additions & 0 deletions opal/browser/dom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
require 'browser/dom/cdata'
require 'browser/dom/comment'
require 'browser/dom/element'
require 'browser/dom/document_or_shadow_root'
require 'browser/dom/document'
require 'browser/dom/document_fragment'
require 'browser/dom/shadow_root'
require 'browser/dom/builder'
require 'browser/dom/mutation_observer'

Expand Down
33 changes: 22 additions & 11 deletions opal/browser/dom/document.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Browser; module DOM

class Document < Element
include DocumentOrShadowRoot

# Get the first element matching the given ID, CSS selector or XPath.
#
# @param what [String] ID, CSS selector or XPath
Expand Down Expand Up @@ -29,14 +31,24 @@ def body
# Create a new element for the document.
#
# @param name [String] the node name
# @param options [Hash] optional `:namespace` name
# @param options [String] :namespace optional namespace name
# @param options [String] :is optional WebComponents is parameter
# @param options [String] :id optional id to set
# @param options [Array<String>] :classes optional classes to set
# @param options [Hash] :attrs optional attributes to set
#
# @return [Element]
def create_element(name, **options)
opts = {}

if options[:is] ||= (options.dig(:attrs, :fragment))
opts[:is] = options[:is]
end

if ns = options[:namespace]
elem = `#@native.createElementNS(#{ns}, #{name})`
elem = `#@native.createElementNS(#{ns}, #{name}, #{opts.to_n})`
else
elem = `#@native.createElement(name)`
elem = `#@native.createElement(name, #{opts.to_n})`
end

if options[:classes]
Expand All @@ -60,6 +72,13 @@ def create_element(name, **options)
DOM(elem)
end

# Create a new document fragment.
#
# @return [DocumentFragment]
def create_document_fragment
DOM(`#@native.createDocumentFragment()`)
end

# Create a new text node for the document.
#
# @param content [String] the text content
Expand Down Expand Up @@ -131,14 +150,6 @@ def root=(element)
`#@native.documentElement = #{Native.convert(element)}`
end

# @!attribute [r] style_sheets
# @return [Array<CSS::StyleSheet>] the style sheets for the document
def style_sheets
Native::Array.new(`#@native.styleSheets`) {|e|
CSS::StyleSheet.new(e)
}
end

# @!attribute title
# @return [String] the document title
def title
Expand Down
18 changes: 18 additions & 0 deletions opal/browser/dom/document_fragment.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
module Browser; module DOM

# TODO: DocumentFragment is not a subclass of Element, but
# a subclass of Node. It implements a ParentNode.
#
# @see https://github.com/opal/opal-browser/pull/46
class DocumentFragment < Element
def self.new(node)
if self == DocumentFragment
if defined? `#{node}.mode`
ShadowRoot.new(node)
else
super
end
else
super
end
end

def self.create
$document.create_document_fragment
end
end

end; end
19 changes: 19 additions & 0 deletions opal/browser/dom/document_or_shadow_root.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Browser; module DOM

# Document and ShadowRoot have some methods and properties in common.
# This solution mimics how it's done in DOM.
#
# @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot
module DocumentOrShadowRoot
# @!attribute [r] style_sheets
# @return [Array<CSS::StyleSheet>] the style sheets for the document
def style_sheets
Native::Array.new(`#@native.styleSheets`) {|e|
CSS::StyleSheet.new(e)
}
end

alias stylesheets style_sheets
end

end; end
21 changes: 21 additions & 0 deletions opal/browser/dom/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,27 @@ def search(*selectors)

alias set_attribute []=

# Creates or accesses the shadow root of this element
#
# @param open [Boolean] set to false if you want to create a closed
# shadow root
#
# @return [ShadowRoot]
def shadow (open = true)
if root = `#@native.shadowRoot`
DOM(root)
else
DOM(`#@native.attachShadow({mode: #{open ? "open" : "closed"}})`)
end
end

# Checks for a presence of a shadow root of this element
#
# @return [Boolean]
def shadow?
`!!#@native.shadowRoot`
end

# @overload style()
#
# Return the style for the element.
Expand Down
12 changes: 12 additions & 0 deletions opal/browser/dom/shadow_root.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Browser; module DOM

class ShadowRoot < DocumentFragment
include DocumentOrShadowRoot

# Use: Element#shadow
def self.create
raise ArgumentError
end
end

end; end
42 changes: 42 additions & 0 deletions spec/dom/element_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,46 @@
expect($document.at_xpath('//div', '//span', '//[@id="lol"]')).to be_a(DOM::Element)
end
end

describe '#shadow' do
html <<-HTML
<div id="shadowtest"></div>
HTML

it 'creates a shadow root' do
expect($document[:shadowtest].shadow?).to be(false)
expect($document[:shadowtest].shadow).to be_a(DOM::ShadowRoot)
expect($document[:shadowtest].shadow?).to be(true)
end

it 'accesses a shadow root' do
$document[:shadowtest].shadow # Create one
expect($document[:shadowtest].shadow?).to be(true)
expect($document[:shadowtest].shadow).to be_a(DOM::ShadowRoot)
end

it 'works like a typical opal-browser DOM tree' do
DOM {
div.shadow_item "Hello world!"
}.append_to($document[:shadowtest].shadow)

expect($document[:shadowtest].at_css(".shadow_item")).to be_nil
expect($document[:shadowtest].shadow.at_css(".shadow_item").text).to be("Hello world!")
end

it 'supports stylesheets' do
$document[:shadowtest].shadow << CSS {
rule("p") {
color "rgb(255, 0, 0)"
}
rule(":host") {
color "rgb(0, 0, 255)"
}
} << DOM { p }

expect($document[:shadowtest].shadow.at_css("p").style!.color).to be("rgb(255, 0, 0)")
expect($document[:shadowtest].style!.color).to be("rgb(0, 0, 255)")
expect($document[:shadowtest].shadow.stylesheets.count).to be(1)
end
end
end

0 comments on commit e48c3c7

Please sign in to comment.