dommy 0.5.0 → 0.7.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 +4 -4
- data/README.md +31 -13
- data/lib/dommy/animation.rb +288 -0
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +396 -0
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +190 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +356 -53
- data/lib/dommy/event.rb +431 -25
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +76 -6
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +82 -0
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +130 -67
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +4 -4
- data/lib/dommy/internal/idna.rb +443 -0
- data/lib/dommy/internal/idna_data.rb +10379 -0
- data/lib/dommy/internal/ipv4_parser.rb +78 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/observable_callback.rb +25 -0
- data/lib/dommy/internal/punycode.rb +202 -0
- data/lib/dommy/internal/range_text_serializer.rb +72 -0
- data/lib/dommy/internal/reflected_attributes.rb +45 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +8 -142
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
- data/lib/dommy/navigator.rb +365 -5
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +89 -0
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +146 -0
- data/lib/dommy/performance_observer.rb +55 -0
- data/lib/dommy/range.rb +597 -0
- data/lib/dommy/resize_observer.rb +53 -0
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +274 -29
- data/lib/dommy/url_pattern.rb +144 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +209 -0
- data/lib/dommy/window.rb +369 -0
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +438 -0
- data/lib/dommy.rb +43 -5
- metadata +44 -29
- data/lib/dommy/world.rb +0 -209
|
@@ -5,7 +5,7 @@ require "uri"
|
|
|
5
5
|
module Dommy
|
|
6
6
|
# `window.location` polyfill. The Window owns one Location and one
|
|
7
7
|
# History instance, and they share the same underlying state. Hash
|
|
8
|
-
# / pushState / replaceState all flow through `
|
|
8
|
+
# / pushState / replaceState all flow through `__internal_set_url__`.
|
|
9
9
|
class Location
|
|
10
10
|
def initialize(window, origin: "http://localhost", pathname: "/", search: "", hash: "")
|
|
11
11
|
@window = window
|
|
@@ -41,7 +41,7 @@ module Dommy
|
|
|
41
41
|
def __js_set__(key, value)
|
|
42
42
|
case key
|
|
43
43
|
when "href"
|
|
44
|
-
|
|
44
|
+
__internal_set_url__(value.to_s)
|
|
45
45
|
when "hash"
|
|
46
46
|
new_hash = value.to_s
|
|
47
47
|
new_hash = "##{new_hash}" unless new_hash.empty? || new_hash.start_with?("#")
|
|
@@ -68,7 +68,7 @@ module Dommy
|
|
|
68
68
|
def __js_call__(method, args)
|
|
69
69
|
case method
|
|
70
70
|
when "assign", "replace"
|
|
71
|
-
|
|
71
|
+
__internal_set_url__(args[0].to_s)
|
|
72
72
|
when "reload"
|
|
73
73
|
nil
|
|
74
74
|
when "toString"
|
|
@@ -83,12 +83,16 @@ module Dommy
|
|
|
83
83
|
# Internal — accepts an absolute or relative URL string and
|
|
84
84
|
# updates pathname / search / hash. Called by History pushState /
|
|
85
85
|
# replaceState and by `location.href = ...`.
|
|
86
|
-
def
|
|
86
|
+
def __internal_set_url__(raw)
|
|
87
87
|
previous_hash = @hash
|
|
88
88
|
if raw.start_with?("#")
|
|
89
89
|
@hash = raw
|
|
90
90
|
else
|
|
91
91
|
uri = URI.join(@origin + @pathname + @search + @hash, raw) rescue URI(raw)
|
|
92
|
+
# An absolute URL (carrying scheme + host) navigates to a new
|
|
93
|
+
# origin; a relative URL inherits the current origin from the
|
|
94
|
+
# join base, so rebuilding with the same parts is a no-op.
|
|
95
|
+
rebuild_origin(scheme: uri.scheme, host: uri.host, port: uri.port) if uri.scheme && uri.host
|
|
92
96
|
@pathname = uri.path.to_s == "" ? "/" : uri.path
|
|
93
97
|
@search = uri.query ? "?#{uri.query}" : ""
|
|
94
98
|
@hash = uri.fragment ? "##{uri.fragment}" : ""
|
|
@@ -134,142 +138,4 @@ module Dommy
|
|
|
134
138
|
rebuild_origin(scheme: scheme, host: parts[:host], port: parts[:port])
|
|
135
139
|
end
|
|
136
140
|
end
|
|
137
|
-
|
|
138
|
-
# `window.history` polyfill. Stack-based; back/forward move the
|
|
139
|
-
# cursor. pushState appends; replaceState mutates the current entry.
|
|
140
|
-
# Each entry is `{ state:, url: }`. Popstate fires when back /
|
|
141
|
-
# forward triggers a different cursor (not on pushState per spec).
|
|
142
|
-
class History
|
|
143
|
-
def initialize(window, location)
|
|
144
|
-
@window = window
|
|
145
|
-
@location = location
|
|
146
|
-
# Initial entry mirrors the live Location. Bookmark URL is
|
|
147
|
-
# resynthesized lazily from Location each time we read it.
|
|
148
|
-
@stack = [{state: nil, url: nil}]
|
|
149
|
-
@cursor = 0
|
|
150
|
-
@scroll_restoration = "auto"
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def __js_get__(key)
|
|
154
|
-
case key
|
|
155
|
-
when "length"
|
|
156
|
-
@stack.size
|
|
157
|
-
when "state"
|
|
158
|
-
@stack[@cursor][:state]
|
|
159
|
-
when "scrollRestoration"
|
|
160
|
-
@scroll_restoration
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def __js_set__(key, value)
|
|
165
|
-
case key
|
|
166
|
-
when "scrollRestoration"
|
|
167
|
-
# Per spec, only "auto" and "manual" are accepted. Invalid
|
|
168
|
-
# values silently retain the current value.
|
|
169
|
-
v = value.to_s
|
|
170
|
-
@scroll_restoration = v if %w[auto manual].include?(v)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
nil
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def __js_call__(method, args)
|
|
177
|
-
case method
|
|
178
|
-
when "pushState"
|
|
179
|
-
push(args[0], args[2])
|
|
180
|
-
when "replaceState"
|
|
181
|
-
replace(args[0], args[2])
|
|
182
|
-
when "back"
|
|
183
|
-
go(-1)
|
|
184
|
-
when "forward"
|
|
185
|
-
go(1)
|
|
186
|
-
when "go"
|
|
187
|
-
go((args[0] || 0).to_i)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
private
|
|
192
|
-
|
|
193
|
-
def push(state, url)
|
|
194
|
-
@stack = @stack[0..@cursor]
|
|
195
|
-
@location.__set_url__(url.to_s) if url
|
|
196
|
-
@stack << {state: state, url: nil}
|
|
197
|
-
@cursor = @stack.size - 1
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def replace(state, url)
|
|
201
|
-
@location.__set_url__(url.to_s) if url
|
|
202
|
-
@stack[@cursor] = {state: state, url: nil}
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def go(delta)
|
|
206
|
-
target = @cursor + delta
|
|
207
|
-
return if target < 0 || target >= @stack.size
|
|
208
|
-
|
|
209
|
-
@cursor = target
|
|
210
|
-
@window.fire_popstate(@stack[@cursor][:state])
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# `URL` constructor — Ruby `URI` wrap. Browser URL API surface:
|
|
215
|
-
# `[:origin]`, `[:pathname]`, `[:search]`, `[:hash]`, `[:href]`.
|
|
216
|
-
# Supports the two-arg form `new URL(raw, base)`.
|
|
217
|
-
class Url
|
|
218
|
-
def initialize(raw, base = nil)
|
|
219
|
-
uri = if base && !base.empty?
|
|
220
|
-
URI.join(base, raw)
|
|
221
|
-
else
|
|
222
|
-
URI(raw.to_s)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
@origin = origin_of(uri)
|
|
226
|
-
@pathname = uri.path.to_s == "" ? "/" : uri.path
|
|
227
|
-
@search = uri.query ? "?#{uri.query}" : ""
|
|
228
|
-
@hash = uri.fragment ? "##{uri.fragment}" : ""
|
|
229
|
-
@href = uri.to_s
|
|
230
|
-
rescue URI::InvalidURIError, ArgumentError
|
|
231
|
-
@origin = ""
|
|
232
|
-
@pathname = ""
|
|
233
|
-
@search = ""
|
|
234
|
-
@hash = ""
|
|
235
|
-
@href = raw.to_s
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def __js_get__(key)
|
|
239
|
-
case key
|
|
240
|
-
when "origin"
|
|
241
|
-
@origin
|
|
242
|
-
when "pathname"
|
|
243
|
-
@pathname
|
|
244
|
-
when "search"
|
|
245
|
-
@search
|
|
246
|
-
when "hash"
|
|
247
|
-
@hash
|
|
248
|
-
when "href"
|
|
249
|
-
@href
|
|
250
|
-
when "toString"
|
|
251
|
-
@href
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def __js_set__(_key, _value)
|
|
256
|
-
nil
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def __js_call__(method, _args)
|
|
260
|
-
method == "toString" ? @href : nil
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
private
|
|
264
|
-
|
|
265
|
-
def origin_of(uri)
|
|
266
|
-
scheme = uri.scheme
|
|
267
|
-
host = uri.host
|
|
268
|
-
return "" unless scheme && host
|
|
269
|
-
|
|
270
|
-
port = uri.port
|
|
271
|
-
default = (scheme == "https" ? 443 : 80)
|
|
272
|
-
port == default ? "#{scheme}://#{host}" : "#{scheme}://#{host}:#{port}"
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
141
|
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `MediaQueryList` — what `window.matchMedia(query)` returns.
|
|
5
|
+
#
|
|
6
|
+
# Dommy has no layout / viewport, so `matches` is `false` by default.
|
|
7
|
+
# Tests drive media query changes via `__test_set_matches__(bool)`, which
|
|
8
|
+
# flips the boolean and fires a `change` event — exactly the surface
|
|
9
|
+
# libraries like Material-UI / Bootstrap / @testing-library consult.
|
|
10
|
+
#
|
|
11
|
+
# Spec: https://drafts.csswg.org/cssom-view/#mediaquerylist
|
|
12
|
+
class MediaQueryList
|
|
13
|
+
include EventTarget
|
|
14
|
+
|
|
15
|
+
attr_reader :media
|
|
16
|
+
|
|
17
|
+
def initialize(window, query)
|
|
18
|
+
@window = window
|
|
19
|
+
@media = query.to_s
|
|
20
|
+
@matches = false
|
|
21
|
+
@onchange = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def matches
|
|
25
|
+
@matches
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias matches? matches
|
|
29
|
+
|
|
30
|
+
# Spec aliases for legacy support.
|
|
31
|
+
def add_listener(callback)
|
|
32
|
+
add_event_listener("change", callback)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias addListener add_listener
|
|
36
|
+
|
|
37
|
+
def remove_listener(callback)
|
|
38
|
+
remove_event_listener("change", callback)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias removeListener remove_listener
|
|
42
|
+
|
|
43
|
+
# Test seam: flip the match state and dispatch a `change` event so
|
|
44
|
+
# subscribers re-render.
|
|
45
|
+
def __test_set_matches__(value)
|
|
46
|
+
return if @matches == !!value
|
|
47
|
+
|
|
48
|
+
@matches = !!value
|
|
49
|
+
dispatch_event(MediaQueryListEvent.new("change", "matches" => @matches, "media" => @media))
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def __js_get__(key)
|
|
54
|
+
case key
|
|
55
|
+
when "media"
|
|
56
|
+
@media
|
|
57
|
+
when "matches"
|
|
58
|
+
@matches
|
|
59
|
+
when "onchange"
|
|
60
|
+
@onchange
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def __js_set__(key, value)
|
|
65
|
+
case key
|
|
66
|
+
when "onchange"
|
|
67
|
+
remove_event_listener("change", @onchange) if @onchange
|
|
68
|
+
@onchange = value
|
|
69
|
+
add_event_listener("change", value) if value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def __js_call__(method, args)
|
|
76
|
+
case method
|
|
77
|
+
when "matches"
|
|
78
|
+
@matches
|
|
79
|
+
when "addListener"
|
|
80
|
+
add_listener(args[0])
|
|
81
|
+
when "removeListener"
|
|
82
|
+
remove_listener(args[0])
|
|
83
|
+
when "addEventListener"
|
|
84
|
+
add_event_listener(args[0], args[1], args[2])
|
|
85
|
+
when "removeEventListener"
|
|
86
|
+
remove_event_listener(args[0], args[1])
|
|
87
|
+
when "dispatchEvent"
|
|
88
|
+
dispatch_event(args[0])
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def __internal_event_parent__
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `MediaQueryListEvent` — the `change` event payload.
|
|
98
|
+
class MediaQueryListEvent < Event
|
|
99
|
+
def initialize(type, init = nil)
|
|
100
|
+
super
|
|
101
|
+
@matches = !!read_init(init, "matches")
|
|
102
|
+
@media = (read_init(init, "media") || "").to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
attr_reader :matches, :media
|
|
106
|
+
|
|
107
|
+
def __js_get__(key)
|
|
108
|
+
case key
|
|
109
|
+
when "matches"
|
|
110
|
+
@matches
|
|
111
|
+
when "media"
|
|
112
|
+
@media
|
|
113
|
+
else
|
|
114
|
+
super
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `MessageChannel` — creates a pair of `MessagePort`s connected to
|
|
5
|
+
# each other. `port1.postMessage(x)` queues a microtask that fires
|
|
6
|
+
# a `message` event on `port2`, and vice versa.
|
|
7
|
+
#
|
|
8
|
+
# Spec: https://html.spec.whatwg.org/multipage/web-messaging.html
|
|
9
|
+
class MessageChannel
|
|
10
|
+
attr_reader :port1, :port2
|
|
11
|
+
|
|
12
|
+
def initialize(window)
|
|
13
|
+
@port1 = MessagePort.new(window)
|
|
14
|
+
@port2 = MessagePort.new(window)
|
|
15
|
+
@port1.__internal_entangle__(@port2)
|
|
16
|
+
@port2.__internal_entangle__(@port1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def __js_get__(key)
|
|
20
|
+
case key
|
|
21
|
+
when "port1"
|
|
22
|
+
@port1
|
|
23
|
+
when "port2"
|
|
24
|
+
@port2
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `MessagePort` — one end of a MessageChannel. `postMessage(value)`
|
|
30
|
+
# dispatches a `MessageEvent` on the entangled port asynchronously.
|
|
31
|
+
class MessagePort
|
|
32
|
+
include EventTarget
|
|
33
|
+
|
|
34
|
+
def initialize(window)
|
|
35
|
+
@window = window
|
|
36
|
+
@entangled = nil
|
|
37
|
+
@onmessage = nil
|
|
38
|
+
@started = false
|
|
39
|
+
@pending = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def __internal_entangle__(other)
|
|
43
|
+
@entangled = other
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def post_message(data)
|
|
47
|
+
port = @entangled
|
|
48
|
+
return unless port
|
|
49
|
+
|
|
50
|
+
@window.scheduler.queue_microtask(
|
|
51
|
+
proc do
|
|
52
|
+
evt = MessageEvent.new("message", "data" => Dommy.structured_clone(data))
|
|
53
|
+
if port.__internal_started?
|
|
54
|
+
port.dispatch_event(evt)
|
|
55
|
+
else
|
|
56
|
+
port.__internal_enqueue__(evt)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias postMessage post_message
|
|
65
|
+
|
|
66
|
+
def start
|
|
67
|
+
@started = true
|
|
68
|
+
flush = @pending
|
|
69
|
+
@pending = []
|
|
70
|
+
flush.each { |evt| dispatch_event(evt) }
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close
|
|
75
|
+
@entangled = nil
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def __internal_started?
|
|
80
|
+
@started || !@inline_message_handler.nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def __internal_enqueue__(event)
|
|
84
|
+
@pending << event
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def __js_get__(key)
|
|
88
|
+
case key
|
|
89
|
+
when "onmessage"
|
|
90
|
+
@onmessage
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def __js_set__(key, value)
|
|
95
|
+
case key
|
|
96
|
+
when "onmessage"
|
|
97
|
+
# Setting onmessage implicitly starts the port per spec.
|
|
98
|
+
remove_event_listener("message", @onmessage) if @onmessage
|
|
99
|
+
@onmessage = value
|
|
100
|
+
@inline_message_handler = value
|
|
101
|
+
add_event_listener("message", value) if value
|
|
102
|
+
start if value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def __js_call__(method, args)
|
|
109
|
+
case method
|
|
110
|
+
when "postMessage"
|
|
111
|
+
post_message(args[0])
|
|
112
|
+
when "start"
|
|
113
|
+
start
|
|
114
|
+
when "close"
|
|
115
|
+
close
|
|
116
|
+
when "addEventListener"
|
|
117
|
+
add_event_listener(args[0], args[1], args[2])
|
|
118
|
+
when "removeEventListener"
|
|
119
|
+
remove_event_listener(args[0], args[1])
|
|
120
|
+
when "dispatchEvent"
|
|
121
|
+
dispatch_event(args[0])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def __internal_event_parent__
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# `MessageEvent` — payload of `message` events on MessagePort /
|
|
131
|
+
# BroadcastChannel / WebSocket / EventSource.
|
|
132
|
+
class MessageEvent < Event
|
|
133
|
+
def initialize(type, init = nil)
|
|
134
|
+
super
|
|
135
|
+
@data = read_init(init, "data")
|
|
136
|
+
@origin = (read_init(init, "origin") || "").to_s
|
|
137
|
+
@last_event_id = (read_init(init, "lastEventId") || "").to_s
|
|
138
|
+
@source = read_init(init, "source")
|
|
139
|
+
@ports = read_init(init, "ports") || []
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
attr_reader :data, :origin, :last_event_id, :source, :ports
|
|
143
|
+
|
|
144
|
+
def __js_get__(key)
|
|
145
|
+
case key
|
|
146
|
+
when "data"
|
|
147
|
+
@data
|
|
148
|
+
when "origin"
|
|
149
|
+
@origin
|
|
150
|
+
when "lastEventId"
|
|
151
|
+
@last_event_id
|
|
152
|
+
when "source"
|
|
153
|
+
@source
|
|
154
|
+
when "ports"
|
|
155
|
+
@ports
|
|
156
|
+
else
|
|
157
|
+
super
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `BroadcastChannel` — same-origin pub/sub. Dommy keeps a per-window
|
|
163
|
+
# channel registry; sending posts to all other peers on the same
|
|
164
|
+
# name within the same Window.
|
|
165
|
+
class BroadcastChannel
|
|
166
|
+
include EventTarget
|
|
167
|
+
|
|
168
|
+
@@registries = Hash.new { |h, w| h[w] = Hash.new { |c, n| c[n] = [] } }
|
|
169
|
+
|
|
170
|
+
attr_reader :name
|
|
171
|
+
|
|
172
|
+
def initialize(window, name)
|
|
173
|
+
@window = window
|
|
174
|
+
@name = name.to_s
|
|
175
|
+
@closed = false
|
|
176
|
+
@onmessage = nil
|
|
177
|
+
@@registries[window][@name] << self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def post_message(data)
|
|
181
|
+
return if @closed
|
|
182
|
+
|
|
183
|
+
peers = @@registries[@window][@name].reject { |p| p.equal?(self) || p.closed? }
|
|
184
|
+
cloned = Dommy.structured_clone(data)
|
|
185
|
+
peers.each do |peer|
|
|
186
|
+
@window.scheduler.queue_microtask(
|
|
187
|
+
proc do
|
|
188
|
+
peer.dispatch_event(MessageEvent.new("message", "data" => cloned))
|
|
189
|
+
end
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
alias postMessage post_message
|
|
197
|
+
|
|
198
|
+
def close
|
|
199
|
+
return if @closed
|
|
200
|
+
|
|
201
|
+
@closed = true
|
|
202
|
+
@@registries[@window][@name].delete(self)
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def closed?
|
|
207
|
+
@closed
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def __js_get__(key)
|
|
211
|
+
case key
|
|
212
|
+
when "name"
|
|
213
|
+
@name
|
|
214
|
+
when "onmessage"
|
|
215
|
+
@onmessage
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def __js_set__(key, value)
|
|
220
|
+
case key
|
|
221
|
+
when "onmessage"
|
|
222
|
+
remove_event_listener("message", @onmessage) if @onmessage
|
|
223
|
+
@onmessage = value
|
|
224
|
+
add_event_listener("message", value) if value
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def __js_call__(method, args)
|
|
231
|
+
case method
|
|
232
|
+
when "postMessage"
|
|
233
|
+
post_message(args[0])
|
|
234
|
+
when "close"
|
|
235
|
+
close
|
|
236
|
+
when "addEventListener"
|
|
237
|
+
add_event_listener(args[0], args[1], args[2])
|
|
238
|
+
when "removeEventListener"
|
|
239
|
+
remove_event_listener(args[0], args[1])
|
|
240
|
+
when "dispatchEvent"
|
|
241
|
+
dispatch_event(args[0])
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def __internal_event_parent__
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -148,17 +148,27 @@ module Dommy
|
|
|
148
148
|
raise TypeError, "MutationObserver.observe: at least one of childList, attributes, characterData must be true"
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
entry = {
|
|
152
|
+
target: target,
|
|
153
|
+
child_list: child_list_on,
|
|
154
|
+
subtree: truthy_option(opts, "subtree"),
|
|
155
|
+
attributes: attributes_on,
|
|
156
|
+
attribute_filter: attribute_filter,
|
|
157
|
+
attribute_old_value: truthy_option(opts, "attributeOldValue"),
|
|
158
|
+
character_data: character_data_on,
|
|
159
|
+
character_data_old_value: truthy_option(opts, "characterDataOldValue")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# WHATWG MutationObserver §observe: if `target` is already
|
|
163
|
+
# observed, replace the existing registration's options
|
|
164
|
+
# (don't merge or stack).
|
|
165
|
+
existing_index = @observed.index { |e| e[:target].equal?(target) }
|
|
166
|
+
if existing_index
|
|
167
|
+
@observed[existing_index] = entry
|
|
168
|
+
else
|
|
169
|
+
@observed << entry
|
|
170
|
+
end
|
|
171
|
+
|
|
162
172
|
@document.register_observer(self)
|
|
163
173
|
nil
|
|
164
174
|
end
|