Skip to content

Commit e48c3c7

Browse files
committed
Shadow DOM implementation.
Notifying issues: opal#46, opal#82
1 parent 85ccf1a commit e48c3c7

File tree

8 files changed

+137
-12
lines changed

8 files changed

+137
-12
lines changed

opal/browser/css/style_sheet.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class StyleSheet
44
include Browser::NativeCachedWrapper
55

66
def initialize(what)
7-
if what.is_a? DOM::Element
7+
if DOM::Element === what
88
super(`#{what.to_n}.sheet`)
99
else
1010
super(what)

opal/browser/dom.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
require 'browser/dom/cdata'
77
require 'browser/dom/comment'
88
require 'browser/dom/element'
9+
require 'browser/dom/document_or_shadow_root'
910
require 'browser/dom/document'
1011
require 'browser/dom/document_fragment'
12+
require 'browser/dom/shadow_root'
1113
require 'browser/dom/builder'
1214
require 'browser/dom/mutation_observer'
1315

opal/browser/dom/document.rb

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module Browser; module DOM
22

33
class Document < Element
4+
include DocumentOrShadowRoot
5+
46
# Get the first element matching the given ID, CSS selector or XPath.
57
#
68
# @param what [String] ID, CSS selector or XPath
@@ -29,14 +31,24 @@ def body
2931
# Create a new element for the document.
3032
#
3133
# @param name [String] the node name
32-
# @param options [Hash] optional `:namespace` name
34+
# @param options [String] :namespace optional namespace name
35+
# @param options [String] :is optional WebComponents is parameter
36+
# @param options [String] :id optional id to set
37+
# @param options [Array<String>] :classes optional classes to set
38+
# @param options [Hash] :attrs optional attributes to set
3339
#
3440
# @return [Element]
3541
def create_element(name, **options)
42+
opts = {}
43+
44+
if options[:is] ||= (options.dig(:attrs, :fragment))
45+
opts[:is] = options[:is]
46+
end
47+
3648
if ns = options[:namespace]
37-
elem = `#@native.createElementNS(#{ns}, #{name})`
49+
elem = `#@native.createElementNS(#{ns}, #{name}, #{opts.to_n})`
3850
else
39-
elem = `#@native.createElement(name)`
51+
elem = `#@native.createElement(name, #{opts.to_n})`
4052
end
4153

4254
if options[:classes]
@@ -60,6 +72,13 @@ def create_element(name, **options)
6072
DOM(elem)
6173
end
6274

75+
# Create a new document fragment.
76+
#
77+
# @return [DocumentFragment]
78+
def create_document_fragment
79+
DOM(`#@native.createDocumentFragment()`)
80+
end
81+
6382
# Create a new text node for the document.
6483
#
6584
# @param content [String] the text content
@@ -131,14 +150,6 @@ def root=(element)
131150
`#@native.documentElement = #{Native.convert(element)}`
132151
end
133152

134-
# @!attribute [r] style_sheets
135-
# @return [Array<CSS::StyleSheet>] the style sheets for the document
136-
def style_sheets
137-
Native::Array.new(`#@native.styleSheets`) {|e|
138-
CSS::StyleSheet.new(e)
139-
}
140-
end
141-
142153
# @!attribute title
143154
# @return [String] the document title
144155
def title

opal/browser/dom/document_fragment.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
module Browser; module DOM
22

3+
# TODO: DocumentFragment is not a subclass of Element, but
4+
# a subclass of Node. It implements a ParentNode.
5+
#
6+
# @see https://github.com/opal/opal-browser/pull/46
37
class DocumentFragment < Element
8+
def self.new(node)
9+
if self == DocumentFragment
10+
if defined? `#{node}.mode`
11+
ShadowRoot.new(node)
12+
else
13+
super
14+
end
15+
else
16+
super
17+
end
18+
end
419

20+
def self.create
21+
$document.create_document_fragment
22+
end
523
end
624

725
end; end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Browser; module DOM
2+
3+
# Document and ShadowRoot have some methods and properties in common.
4+
# This solution mimics how it's done in DOM.
5+
#
6+
# @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot
7+
module DocumentOrShadowRoot
8+
# @!attribute [r] style_sheets
9+
# @return [Array<CSS::StyleSheet>] the style sheets for the document
10+
def style_sheets
11+
Native::Array.new(`#@native.styleSheets`) {|e|
12+
CSS::StyleSheet.new(e)
13+
}
14+
end
15+
16+
alias stylesheets style_sheets
17+
end
18+
19+
end; end

opal/browser/dom/element.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,27 @@ def search(*selectors)
398398

399399
alias set_attribute []=
400400

401+
# Creates or accesses the shadow root of this element
402+
#
403+
# @param open [Boolean] set to false if you want to create a closed
404+
# shadow root
405+
#
406+
# @return [ShadowRoot]
407+
def shadow (open = true)
408+
if root = `#@native.shadowRoot`
409+
DOM(root)
410+
else
411+
DOM(`#@native.attachShadow({mode: #{open ? "open" : "closed"}})`)
412+
end
413+
end
414+
415+
# Checks for a presence of a shadow root of this element
416+
#
417+
# @return [Boolean]
418+
def shadow?
419+
`!!#@native.shadowRoot`
420+
end
421+
401422
# @overload style()
402423
#
403424
# Return the style for the element.

opal/browser/dom/shadow_root.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module Browser; module DOM
2+
3+
class ShadowRoot < DocumentFragment
4+
include DocumentOrShadowRoot
5+
6+
# Use: Element#shadow
7+
def self.create
8+
raise ArgumentError
9+
end
10+
end
11+
12+
end; end

spec/dom/element_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,46 @@
178178
expect($document.at_xpath('//div', '//span', '//[@id="lol"]')).to be_a(DOM::Element)
179179
end
180180
end
181+
182+
describe '#shadow' do
183+
html <<-HTML
184+
<div id="shadowtest"></div>
185+
HTML
186+
187+
it 'creates a shadow root' do
188+
expect($document[:shadowtest].shadow?).to be(false)
189+
expect($document[:shadowtest].shadow).to be_a(DOM::ShadowRoot)
190+
expect($document[:shadowtest].shadow?).to be(true)
191+
end
192+
193+
it 'accesses a shadow root' do
194+
$document[:shadowtest].shadow # Create one
195+
expect($document[:shadowtest].shadow?).to be(true)
196+
expect($document[:shadowtest].shadow).to be_a(DOM::ShadowRoot)
197+
end
198+
199+
it 'works like a typical opal-browser DOM tree' do
200+
DOM {
201+
div.shadow_item "Hello world!"
202+
}.append_to($document[:shadowtest].shadow)
203+
204+
expect($document[:shadowtest].at_css(".shadow_item")).to be_nil
205+
expect($document[:shadowtest].shadow.at_css(".shadow_item").text).to be("Hello world!")
206+
end
207+
208+
it 'supports stylesheets' do
209+
$document[:shadowtest].shadow << CSS {
210+
rule("p") {
211+
color "rgb(255, 0, 0)"
212+
}
213+
rule(":host") {
214+
color "rgb(0, 0, 255)"
215+
}
216+
} << DOM { p }
217+
218+
expect($document[:shadowtest].shadow.at_css("p").style!.color).to be("rgb(255, 0, 0)")
219+
expect($document[:shadowtest].style!.color).to be("rgb(0, 0, 255)")
220+
expect($document[:shadowtest].shadow.stylesheets.count).to be(1)
221+
end
222+
end
181223
end

0 commit comments

Comments
 (0)