dommy 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +213 -0
- data/lib/dommy/attr.rb +200 -0
- data/lib/dommy/blob.rb +182 -0
- data/lib/dommy/bridge.rb +141 -0
- data/lib/dommy/css.rb +283 -0
- data/lib/dommy/custom_elements.rb +125 -0
- data/lib/dommy/data_transfer.rb +98 -0
- data/lib/dommy/document.rb +674 -0
- data/lib/dommy/dom_exception.rb +258 -0
- data/lib/dommy/dom_parser.rb +88 -0
- data/lib/dommy/element.rb +1975 -0
- data/lib/dommy/event.rb +589 -0
- data/lib/dommy/fetch.rb +241 -0
- data/lib/dommy/form_data.rb +208 -0
- data/lib/dommy/html_collection.rb +207 -0
- data/lib/dommy/html_elements.rb +4455 -0
- data/lib/dommy/internal/cookie_jar.rb +27 -0
- data/lib/dommy/internal/dom_matching.rb +141 -0
- data/lib/dommy/internal/mutation_coordinator.rb +172 -0
- data/lib/dommy/internal/node_traversal.rb +36 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
- data/lib/dommy/internal/observer_manager.rb +31 -0
- data/lib/dommy/internal/observer_matcher.rb +31 -0
- data/lib/dommy/internal/scope_resolution.rb +27 -0
- data/lib/dommy/internal/shadow_root_registry.rb +35 -0
- data/lib/dommy/internal/template_content_registry.rb +97 -0
- data/lib/dommy/minitest/assertions.rb +105 -0
- data/lib/dommy/minitest.rb +17 -0
- data/lib/dommy/navigator.rb +271 -0
- data/lib/dommy/node.rb +218 -0
- data/lib/dommy/observer.rb +199 -0
- data/lib/dommy/parser.rb +29 -0
- data/lib/dommy/promise.rb +199 -0
- data/lib/dommy/router.rb +275 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
- data/lib/dommy/rspec/matchers.rb +230 -0
- data/lib/dommy/rspec.rb +18 -0
- data/lib/dommy/scheduler.rb +135 -0
- data/lib/dommy/shadow_root.rb +255 -0
- data/lib/dommy/storage.rb +112 -0
- data/lib/dommy/test_helpers.rb +78 -0
- data/lib/dommy/tree_walker.rb +425 -0
- data/lib/dommy/url.rb +479 -0
- data/lib/dommy/version.rb +5 -0
- data/lib/dommy/world.rb +209 -0
- data/lib/dommy.rb +119 -0
- metadata +110 -0
data/lib/dommy/bridge.rb
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `Dommy::Bridge` — adapter classes for JS-style bridges (wasm
|
|
5
|
+
# embedders that route DOM method calls and constructor `new` ops
|
|
6
|
+
# through the `__js_get__` / `__js_set__` / `__js_call__` /
|
|
7
|
+
# `__js_new__` protocol).
|
|
8
|
+
#
|
|
9
|
+
# CRuby users writing happy-dom-style tests can ignore everything
|
|
10
|
+
# in this namespace; it's only relevant when integrating Dommy
|
|
11
|
+
# with an external runtime (such as an mruby-on-wasm host) that
|
|
12
|
+
# constructs callbacks / events / promises via the bridge view.
|
|
13
|
+
#
|
|
14
|
+
# The protocol contract:
|
|
15
|
+
# - `__js_get__(name)` reads a JS-style property by string name
|
|
16
|
+
# - `__js_set__(name, value)` writes one
|
|
17
|
+
# - `__js_call__(method, args)` invokes a method with positional
|
|
18
|
+
# args (Array)
|
|
19
|
+
# - `__js_new__(args)` invokes the value as a JS constructor
|
|
20
|
+
module Bridge
|
|
21
|
+
# Wraps an external callback handle (registered in a host-side
|
|
22
|
+
# callback table) so the JS bridge can resolve / invoke it. The
|
|
23
|
+
# external host that creates these is responsible for honoring
|
|
24
|
+
# `invoke_callback(callback_id, args)`.
|
|
25
|
+
#
|
|
26
|
+
# The `__callback_id__` key on this object exposes the integer
|
|
27
|
+
# id to JS-side code that needs to round-trip it (e.g. for
|
|
28
|
+
# release / introspection).
|
|
29
|
+
class Callback
|
|
30
|
+
ID_KEY = "__callback_id__"
|
|
31
|
+
|
|
32
|
+
def initialize(host, callback_id)
|
|
33
|
+
@host = host
|
|
34
|
+
@callback_id = callback_id
|
|
35
|
+
@props = {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def __js_get__(key)
|
|
39
|
+
if key == ID_KEY
|
|
40
|
+
@props.fetch(key, @callback_id)
|
|
41
|
+
else
|
|
42
|
+
@props[key]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def __js_set__(key, value)
|
|
47
|
+
@props[key] = value
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __js_call__(method, args)
|
|
52
|
+
case method
|
|
53
|
+
when "call"
|
|
54
|
+
@host.invoke_callback(@callback_id, args)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Block-as-constructor adapter — invoking `__js_new__(args)`
|
|
60
|
+
# calls the wrapped block with `args` and returns whatever the
|
|
61
|
+
# block produces. Used by Window to wire up `new Event(init)`,
|
|
62
|
+
# `new CustomEvent(init)`, etc. without hand-rolling a class
|
|
63
|
+
# for each constructor.
|
|
64
|
+
class Constructor
|
|
65
|
+
def initialize(&block)
|
|
66
|
+
@block = block
|
|
67
|
+
@class_methods = {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def __js_new__(args)
|
|
71
|
+
@block.call(args)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Register a class-level method (e.g. `URL.createObjectURL`)
|
|
75
|
+
# that JS bridges resolve via `__js_call__` on the constructor
|
|
76
|
+
# itself. Returns self for chaining.
|
|
77
|
+
def define_class_method(name, &block)
|
|
78
|
+
@class_methods[name.to_s] = block
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def __js_call__(method, args)
|
|
83
|
+
handler = @class_methods[method.to_s]
|
|
84
|
+
handler&.call(args)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `JS.global[:Promise]` view. Implements the `resolve` / `reject`
|
|
89
|
+
# class methods plus `new Promise(executor)` via `__js_new__`.
|
|
90
|
+
class PromiseConstructor
|
|
91
|
+
def initialize(window)
|
|
92
|
+
@window = window
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def __js_call__(method, args)
|
|
96
|
+
case method
|
|
97
|
+
when "resolve"
|
|
98
|
+
PromiseValue.resolve(@window, args[0])
|
|
99
|
+
when "reject"
|
|
100
|
+
PromiseValue.reject(@window, args[0])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# `new Promise(executor)` — runs executor synchronously with
|
|
105
|
+
# (resolve, reject) callbacks.
|
|
106
|
+
def __js_new__(args)
|
|
107
|
+
executor = args[0]
|
|
108
|
+
promise = PromiseValue.new(@window)
|
|
109
|
+
resolve = PromiseSettler.new(promise, fulfilled: true)
|
|
110
|
+
reject = PromiseSettler.new(promise, fulfilled: false)
|
|
111
|
+
if executor.respond_to?(:__js_call__)
|
|
112
|
+
executor.__js_call__("call", [resolve, reject])
|
|
113
|
+
elsif executor.respond_to?(:call)
|
|
114
|
+
executor.call(resolve, reject)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
promise
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Adapter so a Ruby-side executor can deliver resolve/reject
|
|
122
|
+
# through the same `__js_call__("call", args)` interface that
|
|
123
|
+
# the scheduler and JS bridge use for callbacks.
|
|
124
|
+
class PromiseSettler
|
|
125
|
+
def initialize(promise, fulfilled:)
|
|
126
|
+
@promise = promise
|
|
127
|
+
@fulfilled = fulfilled
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def __js_call__(_method, args)
|
|
131
|
+
if @fulfilled
|
|
132
|
+
@promise.fulfill(args[0])
|
|
133
|
+
else
|
|
134
|
+
@promise.reject(args[0])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/dommy/css.rb
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `CSSStyleSheet` — stub implementation. Dommy has no CSS parser
|
|
5
|
+
# nor a render tree, so we don't interpret rule text; the sheet
|
|
6
|
+
# acts as an ordered list of opaque `CSSRule`-like wrappers.
|
|
7
|
+
#
|
|
8
|
+
# Useful for code that does:
|
|
9
|
+
#
|
|
10
|
+
# sheet.insertRule("p { color: red }", 0);
|
|
11
|
+
# for (const r of sheet.cssRules) console.log(r.cssText);
|
|
12
|
+
#
|
|
13
|
+
# `disabled` is honored as state. `href`, `media`, `title`, `type`
|
|
14
|
+
# mirror the owner node's attributes when present.
|
|
15
|
+
class CSSStyleSheet
|
|
16
|
+
attr_reader :owner_node, :css_rules
|
|
17
|
+
|
|
18
|
+
def initialize(owner_node:, href: nil, media: nil, title: nil, type: "text/css")
|
|
19
|
+
@owner_node = owner_node
|
|
20
|
+
@href = href
|
|
21
|
+
@media = media
|
|
22
|
+
@title = title
|
|
23
|
+
@type = type
|
|
24
|
+
@disabled = false
|
|
25
|
+
@css_rules = CSSRuleList.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disabled
|
|
29
|
+
@disabled
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def disabled=(v)
|
|
33
|
+
@disabled = !!v
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def href
|
|
37
|
+
@href
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def title
|
|
41
|
+
@title
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def type
|
|
45
|
+
@type
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def media
|
|
49
|
+
@media.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parent_style_sheet
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def owner_rule
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `insertRule(rule_text, index)` — appends an opaque CSSRule at the
|
|
61
|
+
# given position (default: end). Returns the index used.
|
|
62
|
+
def insert_rule(rule_text, index = nil)
|
|
63
|
+
idx = index.nil? ? @css_rules.length : index.to_i
|
|
64
|
+
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx > @css_rules.length
|
|
65
|
+
|
|
66
|
+
@css_rules.__insert__(idx, CSSRule.new(rule_text.to_s, self))
|
|
67
|
+
idx
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_rule(index)
|
|
71
|
+
idx = index.to_i
|
|
72
|
+
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx >= @css_rules.length
|
|
73
|
+
|
|
74
|
+
@css_rules.__delete_at__(idx)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# `replaceSync(text)` — replace all rules with a single rule blob
|
|
79
|
+
# (no parsing — we keep it as one opaque entry).
|
|
80
|
+
def replace_sync(text)
|
|
81
|
+
@css_rules.__clear__
|
|
82
|
+
return nil if text.to_s.empty?
|
|
83
|
+
|
|
84
|
+
@css_rules.__insert__(0, CSSRule.new(text.to_s, self))
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `replace(text)` — spec returns a Promise resolved with self.
|
|
89
|
+
# We can't return a JS-bridge Promise from here without a Window,
|
|
90
|
+
# so we mirror the sync behavior and return self.
|
|
91
|
+
def replace(text)
|
|
92
|
+
replace_sync(text)
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def __js_get__(key)
|
|
97
|
+
case key
|
|
98
|
+
when "cssRules", "rules"
|
|
99
|
+
@css_rules
|
|
100
|
+
when "disabled"
|
|
101
|
+
@disabled
|
|
102
|
+
when "href"
|
|
103
|
+
@href
|
|
104
|
+
when "media"
|
|
105
|
+
media
|
|
106
|
+
when "title"
|
|
107
|
+
@title
|
|
108
|
+
when "type"
|
|
109
|
+
@type
|
|
110
|
+
when "ownerNode"
|
|
111
|
+
@owner_node
|
|
112
|
+
when "parentStyleSheet"
|
|
113
|
+
parent_style_sheet
|
|
114
|
+
when "ownerRule"
|
|
115
|
+
owner_rule
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def __js_set__(key, value)
|
|
120
|
+
case key
|
|
121
|
+
when "disabled"
|
|
122
|
+
self.disabled = value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def __js_call__(method, args)
|
|
129
|
+
case method
|
|
130
|
+
when "insertRule"
|
|
131
|
+
insert_rule(args[0], args[1])
|
|
132
|
+
when "deleteRule"
|
|
133
|
+
delete_rule(args[0])
|
|
134
|
+
when "replaceSync"
|
|
135
|
+
replace_sync(args[0])
|
|
136
|
+
when "replace"
|
|
137
|
+
replace(args[0])
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# `CSSRuleList` — indexed list of CSSRule, returned by
|
|
143
|
+
# `sheet.cssRules`. Live: mutations to the owning sheet are visible.
|
|
144
|
+
class CSSRuleList
|
|
145
|
+
include Enumerable
|
|
146
|
+
|
|
147
|
+
def initialize
|
|
148
|
+
@rules = []
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def length
|
|
152
|
+
@rules.length
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
alias size length
|
|
156
|
+
|
|
157
|
+
def item(index)
|
|
158
|
+
i = index.to_i
|
|
159
|
+
return nil if i < 0 || i >= @rules.length
|
|
160
|
+
|
|
161
|
+
@rules[i]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def [](index)
|
|
165
|
+
item(index)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def each(&blk)
|
|
169
|
+
@rules.each(&blk)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def to_a
|
|
173
|
+
@rules.dup
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def __insert__(index, rule)
|
|
177
|
+
@rules.insert(index, rule)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def __delete_at__(index)
|
|
181
|
+
@rules.delete_at(index)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def __clear__
|
|
185
|
+
@rules.clear
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def __js_get__(key)
|
|
189
|
+
case key
|
|
190
|
+
when "length"
|
|
191
|
+
length
|
|
192
|
+
else
|
|
193
|
+
if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
|
|
194
|
+
item(key.to_i)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def __js_call__(method, args)
|
|
200
|
+
case method
|
|
201
|
+
when "item"
|
|
202
|
+
item(args[0])
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# `CSSRule` — opaque wrapper over the raw rule text. Real engines
|
|
208
|
+
# have a subclass hierarchy (CSSStyleRule, CSSMediaRule, etc.), but
|
|
209
|
+
# without a CSS parser we keep one minimal type that round-trips
|
|
210
|
+
# the source text.
|
|
211
|
+
class CSSRule
|
|
212
|
+
STYLE_RULE = 1
|
|
213
|
+
CHARSET_RULE = 2
|
|
214
|
+
IMPORT_RULE = 3
|
|
215
|
+
MEDIA_RULE = 4
|
|
216
|
+
FONT_FACE_RULE = 5
|
|
217
|
+
PAGE_RULE = 6
|
|
218
|
+
KEYFRAMES_RULE = 7
|
|
219
|
+
KEYFRAME_RULE = 8
|
|
220
|
+
|
|
221
|
+
attr_reader :parent_style_sheet
|
|
222
|
+
|
|
223
|
+
def initialize(css_text, parent_style_sheet = nil)
|
|
224
|
+
@css_text = css_text.to_s
|
|
225
|
+
@parent_style_sheet = parent_style_sheet
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def css_text
|
|
229
|
+
@css_text
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def css_text=(v)
|
|
233
|
+
@css_text = v.to_s
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# We don't parse, so report the generic STYLE_RULE type.
|
|
237
|
+
def type
|
|
238
|
+
STYLE_RULE
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def parent_rule
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def __js_get__(key)
|
|
246
|
+
case key
|
|
247
|
+
when "cssText"
|
|
248
|
+
@css_text
|
|
249
|
+
when "type"
|
|
250
|
+
type
|
|
251
|
+
when "parentStyleSheet"
|
|
252
|
+
@parent_style_sheet
|
|
253
|
+
when "parentRule"
|
|
254
|
+
parent_rule
|
|
255
|
+
when "STYLE_RULE"
|
|
256
|
+
STYLE_RULE
|
|
257
|
+
when "MEDIA_RULE"
|
|
258
|
+
MEDIA_RULE
|
|
259
|
+
when "IMPORT_RULE"
|
|
260
|
+
IMPORT_RULE
|
|
261
|
+
when "FONT_FACE_RULE"
|
|
262
|
+
FONT_FACE_RULE
|
|
263
|
+
when "PAGE_RULE"
|
|
264
|
+
PAGE_RULE
|
|
265
|
+
when "KEYFRAMES_RULE"
|
|
266
|
+
KEYFRAMES_RULE
|
|
267
|
+
when "KEYFRAME_RULE"
|
|
268
|
+
KEYFRAME_RULE
|
|
269
|
+
when "CHARSET_RULE"
|
|
270
|
+
CHARSET_RULE
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def __js_set__(key, value)
|
|
275
|
+
case key
|
|
276
|
+
when "cssText"
|
|
277
|
+
self.css_text = value
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `window.customElements` — registry mapping custom element tag
|
|
5
|
+
# names to Ruby classes that extend `HTMLElement`. Lifecycle
|
|
6
|
+
# callbacks (`connected_callback` / `disconnected_callback` /
|
|
7
|
+
# `attribute_changed_callback` / `adopted_callback`) are invoked by
|
|
8
|
+
# the document's mutation pipeline when registered elements are
|
|
9
|
+
# added, removed, or have observed attributes mutated.
|
|
10
|
+
#
|
|
11
|
+
# Names must contain a hyphen per the HTML spec (e.g., `my-button`).
|
|
12
|
+
class CustomElementRegistry
|
|
13
|
+
NAME_RE = /\A[a-z][a-z0-9-]*-[a-z0-9-]*\z/
|
|
14
|
+
|
|
15
|
+
def initialize(window)
|
|
16
|
+
@window = window
|
|
17
|
+
# name → klass
|
|
18
|
+
@definitions = {}
|
|
19
|
+
# name → Array<{ resolve, reject }>
|
|
20
|
+
@pending_promises = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def define(name, klass, _options = nil)
|
|
24
|
+
key = name.to_s
|
|
25
|
+
unless key.match?(NAME_RE)
|
|
26
|
+
raise DOMException::SyntaxError, "name must be a hyphenated string, got #{name.inspect}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
raise DOMException::NotSupportedError, "#{key} already defined" if @definitions.key?(key)
|
|
30
|
+
|
|
31
|
+
@definitions[key] = klass
|
|
32
|
+
# Resolve any pending whenDefined() promises and re-wrap
|
|
33
|
+
# already-existing nodes (upgrade).
|
|
34
|
+
resolve_pending(key, klass)
|
|
35
|
+
upgrade_existing(key)
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get(name)
|
|
40
|
+
@definitions[name.to_s]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_name(klass)
|
|
44
|
+
@definitions.each { |k, v| return k if v == klass }
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns a Dommy::PromiseValue that resolves with the registered
|
|
49
|
+
# constructor when `name` is defined (immediately if already so).
|
|
50
|
+
def when_defined(name)
|
|
51
|
+
key = name.to_s
|
|
52
|
+
promise = PromiseValue.new(@window)
|
|
53
|
+
if (klass = @definitions[key])
|
|
54
|
+
promise.fulfill(klass)
|
|
55
|
+
else
|
|
56
|
+
@pending_promises[key] ||= []
|
|
57
|
+
@pending_promises[key] << promise
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
promise
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Walk `root`'s subtree and re-wrap any nodes whose tag is now
|
|
64
|
+
# registered; fires `connectedCallback` for each upgraded node
|
|
65
|
+
# that's currently attached to a document tree.
|
|
66
|
+
def upgrade(root)
|
|
67
|
+
return nil unless root.respond_to?(:__node__)
|
|
68
|
+
|
|
69
|
+
walk_descendants(root.__node__) do |nk|
|
|
70
|
+
next unless nk.element?
|
|
71
|
+
next unless @definitions.key?(nk.name)
|
|
72
|
+
|
|
73
|
+
# Force re-wrap by clearing the document's cached wrapper.
|
|
74
|
+
@window.document.__reset_wrapper__(nk)
|
|
75
|
+
wrapped = @window.document.wrap_node(nk)
|
|
76
|
+
@window.document.__notify_connected__(wrapped) if wrapped
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def __js_get__(_key)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def __js_call__(method, args)
|
|
87
|
+
case method
|
|
88
|
+
when "define"
|
|
89
|
+
define(args[0], args[1], args[2])
|
|
90
|
+
when "get"
|
|
91
|
+
get(args[0])
|
|
92
|
+
when "whenDefined"
|
|
93
|
+
when_defined(args[0])
|
|
94
|
+
when "upgrade"
|
|
95
|
+
upgrade(args[0])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def resolve_pending(name, klass)
|
|
102
|
+
list = @pending_promises.delete(name)
|
|
103
|
+
list&.each { |p| p.fulfill(klass) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# When define() lands after the matching element is already in
|
|
107
|
+
# the document, those nodes need upgrading: re-wrap them with the
|
|
108
|
+
# new class and fire connectedCallback.
|
|
109
|
+
def upgrade_existing(name)
|
|
110
|
+
doc = @window.document
|
|
111
|
+
doc.nokogiri_doc.css(name).each do |nk|
|
|
112
|
+
doc.__reset_wrapper__(nk)
|
|
113
|
+
wrapped = doc.wrap_node(nk)
|
|
114
|
+
doc.__notify_connected__(wrapped) if wrapped
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def walk_descendants(node, &blk)
|
|
119
|
+
yield node
|
|
120
|
+
return unless node.respond_to?(:children)
|
|
121
|
+
|
|
122
|
+
node.children.each { |c| walk_descendants(c, &blk) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `DataTransfer` — the payload object on a DragEvent. Holds the
|
|
5
|
+
# files being dragged plus arbitrary string-keyed data per MIME
|
|
6
|
+
# format. Tests build one explicitly to simulate drag-and-drop:
|
|
7
|
+
#
|
|
8
|
+
# dt = Dommy::DataTransfer.new(files: [file])
|
|
9
|
+
# ev = Dommy::DragEvent.new("drop", "dataTransfer" => dt, "bubbles" => true)
|
|
10
|
+
# target.dispatch_event(ev)
|
|
11
|
+
#
|
|
12
|
+
# Spec: https://html.spec.whatwg.org/multipage/dnd.html#datatransfer
|
|
13
|
+
class DataTransfer
|
|
14
|
+
attr_reader :files
|
|
15
|
+
|
|
16
|
+
def initialize(files: [], data: {})
|
|
17
|
+
@files = files.is_a?(FileList) ? files : FileList.new(Array(files))
|
|
18
|
+
@data = data.transform_keys { |k| normalize_format(k) }
|
|
19
|
+
@drop_effect = "none"
|
|
20
|
+
@effect_allowed = "uninitialized"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def types
|
|
24
|
+
@data.keys
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_data(format)
|
|
28
|
+
@data[normalize_format(format)].to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set_data(format, data)
|
|
32
|
+
@data[normalize_format(format)] = data.to_s
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear_data(format = nil)
|
|
37
|
+
if format
|
|
38
|
+
@data.delete(normalize_format(format))
|
|
39
|
+
else
|
|
40
|
+
@data.clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_accessor :drop_effect, :effect_allowed
|
|
47
|
+
|
|
48
|
+
def __js_get__(key)
|
|
49
|
+
case key
|
|
50
|
+
when "files"
|
|
51
|
+
@files
|
|
52
|
+
when "types"
|
|
53
|
+
types
|
|
54
|
+
when "dropEffect"
|
|
55
|
+
@drop_effect
|
|
56
|
+
when "effectAllowed"
|
|
57
|
+
@effect_allowed
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def __js_set__(key, value)
|
|
62
|
+
case key
|
|
63
|
+
when "dropEffect"
|
|
64
|
+
@drop_effect = value.to_s
|
|
65
|
+
when "effectAllowed"
|
|
66
|
+
@effect_allowed = value.to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def __js_call__(method, args)
|
|
73
|
+
case method
|
|
74
|
+
when "getData"
|
|
75
|
+
get_data(args[0])
|
|
76
|
+
when "setData"
|
|
77
|
+
set_data(args[0], args[1])
|
|
78
|
+
when "clearData"
|
|
79
|
+
clear_data(args[0])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Per spec, "text" maps to "text/plain" and "url" maps to
|
|
86
|
+
# "text/uri-list"; otherwise lowercase the MIME format.
|
|
87
|
+
def normalize_format(format)
|
|
88
|
+
case format.to_s.downcase
|
|
89
|
+
when "text"
|
|
90
|
+
"text/plain"
|
|
91
|
+
when "url"
|
|
92
|
+
"text/uri-list"
|
|
93
|
+
else
|
|
94
|
+
format.to_s.downcase
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|