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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `EventSource` (Server-Sent Events). Like `WebSocket`, dommy
|
|
5
|
+
# provides simulation seams instead of network IO:
|
|
6
|
+
#
|
|
7
|
+
# es.__test_simulate_open__
|
|
8
|
+
# es.__test_simulate_message__(data, event: "msg", id: "1")
|
|
9
|
+
# es.__test_simulate_error__
|
|
10
|
+
#
|
|
11
|
+
# Auto-opens on a microtask after construction, mirroring real
|
|
12
|
+
# browser behavior.
|
|
13
|
+
#
|
|
14
|
+
# Spec: https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
15
|
+
class EventSource
|
|
16
|
+
include EventTarget
|
|
17
|
+
|
|
18
|
+
CONNECTING = 0
|
|
19
|
+
OPEN = 1
|
|
20
|
+
CLOSED = 2
|
|
21
|
+
|
|
22
|
+
INLINE_HANDLERS = %w[open message error].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :url, :ready_state, :with_credentials
|
|
25
|
+
|
|
26
|
+
def initialize(window, url, options = nil)
|
|
27
|
+
@window = window
|
|
28
|
+
@url = url.to_s
|
|
29
|
+
@ready_state = CONNECTING
|
|
30
|
+
opts = options.is_a?(Hash) ? options : {}
|
|
31
|
+
@with_credentials = !!(opts["withCredentials"] || opts[:withCredentials])
|
|
32
|
+
@inline_handlers = {}
|
|
33
|
+
|
|
34
|
+
@window.scheduler.queue_microtask(proc { __test_simulate_open__ })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@ready_state = CLOSED
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- Test seams ------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def __test_simulate_open__
|
|
45
|
+
return if @ready_state != CONNECTING
|
|
46
|
+
|
|
47
|
+
@ready_state = OPEN
|
|
48
|
+
dispatch_event(Event.new("open"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __test_simulate_message__(data, event: "message", id: nil, retry_ms: nil)
|
|
52
|
+
return if @ready_state != OPEN
|
|
53
|
+
|
|
54
|
+
payload = {"data" => data.to_s}
|
|
55
|
+
payload["lastEventId"] = id.to_s if id
|
|
56
|
+
payload["retry"] = retry_ms.to_i if retry_ms
|
|
57
|
+
dispatch_event(MessageEvent.new(event.to_s, payload))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def __test_simulate_error__
|
|
61
|
+
dispatch_event(Event.new("error"))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- JS bridge -------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def __js_get__(key)
|
|
67
|
+
case key
|
|
68
|
+
when "url"
|
|
69
|
+
@url
|
|
70
|
+
when "readyState"
|
|
71
|
+
@ready_state
|
|
72
|
+
when "withCredentials"
|
|
73
|
+
@with_credentials
|
|
74
|
+
when "CONNECTING"
|
|
75
|
+
CONNECTING
|
|
76
|
+
when "OPEN"
|
|
77
|
+
OPEN
|
|
78
|
+
when "CLOSED"
|
|
79
|
+
CLOSED
|
|
80
|
+
else
|
|
81
|
+
@inline_handlers[inline_event_for(key)]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def __js_set__(key, value)
|
|
86
|
+
event = inline_event_for(key)
|
|
87
|
+
set_inline_handler(event, value) if event
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def __js_call__(method, args)
|
|
92
|
+
case method
|
|
93
|
+
when "close"
|
|
94
|
+
close
|
|
95
|
+
when "addEventListener"
|
|
96
|
+
add_event_listener(args[0], args[1], args[2])
|
|
97
|
+
when "removeEventListener"
|
|
98
|
+
remove_event_listener(args[0], args[1])
|
|
99
|
+
when "dispatchEvent"
|
|
100
|
+
dispatch_event(args[0])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def __internal_event_parent__
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
INLINE_EVENT_MAP = INLINE_HANDLERS
|
|
111
|
+
.each_with_object({}) do |name, h|
|
|
112
|
+
h["on#{name}"] = name
|
|
113
|
+
end
|
|
114
|
+
.freeze
|
|
115
|
+
|
|
116
|
+
def inline_event_for(key)
|
|
117
|
+
INLINE_EVENT_MAP[key.to_s]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def set_inline_handler(event, handler)
|
|
121
|
+
previous = @inline_handlers[event]
|
|
122
|
+
remove_event_listener(event, previous) if previous
|
|
123
|
+
if handler.nil?
|
|
124
|
+
@inline_handlers.delete(event)
|
|
125
|
+
else
|
|
126
|
+
add_event_listener(event, handler)
|
|
127
|
+
@inline_handlers[event] = handler
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/dommy/fetch.rb
CHANGED
|
@@ -118,6 +118,68 @@ module Dommy
|
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# `Request` polyfill — minimal Fetch API Request object so callers
|
|
122
|
+
# constructing `new Request(url, init)` get a value with `.url`,
|
|
123
|
+
# `.method`, `.headers`, `.body`. The stub-based `fetch` doesn't
|
|
124
|
+
# consume it directly (it still takes `(url, init)`), but having
|
|
125
|
+
# Request available means JS code that constructs one before
|
|
126
|
+
# passing to fetch keeps working.
|
|
127
|
+
class Request
|
|
128
|
+
attr_reader :url, :method, :body
|
|
129
|
+
|
|
130
|
+
def initialize(url, init = nil)
|
|
131
|
+
opts = init.is_a?(Hash) ? init : {}
|
|
132
|
+
@url = url.to_s
|
|
133
|
+
@method = (opts["method"] || opts[:method] || "GET").to_s.upcase
|
|
134
|
+
@body = opts["body"] || opts[:body]
|
|
135
|
+
raw_headers = opts["headers"] || opts[:headers] || {}
|
|
136
|
+
@headers = Headers.new(raw_headers)
|
|
137
|
+
@credentials = (opts["credentials"] || opts[:credentials] || "same-origin").to_s
|
|
138
|
+
@mode = (opts["mode"] || opts[:mode] || "cors").to_s
|
|
139
|
+
@cache = (opts["cache"] || opts[:cache] || "default").to_s
|
|
140
|
+
@redirect = (opts["redirect"] || opts[:redirect] || "follow").to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
attr_reader :headers, :credentials, :mode, :cache, :redirect
|
|
144
|
+
|
|
145
|
+
def __js_get__(key)
|
|
146
|
+
case key
|
|
147
|
+
when "url"
|
|
148
|
+
@url
|
|
149
|
+
when "method"
|
|
150
|
+
@method
|
|
151
|
+
when "headers"
|
|
152
|
+
@headers
|
|
153
|
+
when "body"
|
|
154
|
+
@body
|
|
155
|
+
when "credentials"
|
|
156
|
+
@credentials
|
|
157
|
+
when "mode"
|
|
158
|
+
@mode
|
|
159
|
+
when "cache"
|
|
160
|
+
@cache
|
|
161
|
+
when "redirect"
|
|
162
|
+
@redirect
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def __js_call__(method, _args)
|
|
167
|
+
case method
|
|
168
|
+
when "clone"
|
|
169
|
+
Request.new(
|
|
170
|
+
@url,
|
|
171
|
+
"method" => @method,
|
|
172
|
+
"body" => @body,
|
|
173
|
+
"headers" => @headers.to_h,
|
|
174
|
+
"credentials" => @credentials,
|
|
175
|
+
"mode" => @mode,
|
|
176
|
+
"cache" => @cache,
|
|
177
|
+
"redirect" => @redirect
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
121
183
|
# `Response` polyfill — just enough surface for Fetchy:
|
|
122
184
|
# `[:status]` / `[:ok]` / `[:url]` / `[:headers]` (with
|
|
123
185
|
# `.entries()` / `.get(name)`) and `.text()` / `.json()` / `.body`
|
|
@@ -165,8 +227,10 @@ module Dommy
|
|
|
165
227
|
rejected(err)
|
|
166
228
|
end
|
|
167
229
|
|
|
168
|
-
when "arrayBuffer"
|
|
169
|
-
immediate(@body)
|
|
230
|
+
when "arrayBuffer"
|
|
231
|
+
immediate(@body.bytes)
|
|
232
|
+
when "blob"
|
|
233
|
+
immediate(Blob.new([@body], "type" => @headers.__js_call__("get", ["content-type"]) || ""))
|
|
170
234
|
when "clone"
|
|
171
235
|
Response.new(
|
|
172
236
|
@window,
|
|
@@ -218,15 +282,21 @@ module Dommy
|
|
|
218
282
|
when "entries"
|
|
219
283
|
@hash.to_a
|
|
220
284
|
when "has"
|
|
221
|
-
|
|
285
|
+
# Match `get`'s case-insensitive lookup: try the raw name
|
|
286
|
+
# first, then the Title-Case canonical form. WHATWG defines
|
|
287
|
+
# header names as case-insensitive throughout the Headers API.
|
|
288
|
+
name = args[0].to_s
|
|
289
|
+
@hash.key?(name) || @hash.key?(Headers.canonical(name))
|
|
222
290
|
when "forEach"
|
|
223
|
-
#
|
|
291
|
+
# WHATWG: forEach(callback) — callback(value, key, headers).
|
|
292
|
+
# Pass `self` as the third argument so consumers that read
|
|
293
|
+
# `(_, _, h) => h.get("Foo")` work the same as in a browser.
|
|
224
294
|
cb = args[0]
|
|
225
295
|
@hash.each do |k, v|
|
|
226
296
|
if cb.respond_to?(:__js_call__)
|
|
227
|
-
cb.__js_call__("call", [v, k])
|
|
297
|
+
cb.__js_call__("call", [v, k, self])
|
|
228
298
|
elsif cb.respond_to?(:call)
|
|
229
|
-
cb.call(v, k)
|
|
299
|
+
cb.call(v, k, self)
|
|
230
300
|
end
|
|
231
301
|
end
|
|
232
302
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `FileReader` — reads `Blob` / `File` instances into Strings, byte
|
|
5
|
+
# arrays, or data URLs. Asynchronous reads schedule a microtask that
|
|
6
|
+
# fires `load` + `loadend`; sync access (via `result` after the
|
|
7
|
+
# microtask drain) mirrors browser behavior.
|
|
8
|
+
#
|
|
9
|
+
# Spec: https://w3c.github.io/FileAPI/#APIASynch
|
|
10
|
+
class FileReader
|
|
11
|
+
include EventTarget
|
|
12
|
+
|
|
13
|
+
EMPTY = 0
|
|
14
|
+
LOADING = 1
|
|
15
|
+
DONE = 2
|
|
16
|
+
|
|
17
|
+
INLINE_HANDLERS = %w[loadstart progress load loadend abort error].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :ready_state, :result, :error
|
|
20
|
+
|
|
21
|
+
def initialize(window)
|
|
22
|
+
@window = window
|
|
23
|
+
@ready_state = EMPTY
|
|
24
|
+
@result = nil
|
|
25
|
+
@error = nil
|
|
26
|
+
@inline_handlers = {}
|
|
27
|
+
@aborted = false
|
|
28
|
+
@generation = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def read_as_text(blob, _encoding = "utf-8")
|
|
32
|
+
schedule_read(blob) { |raw| raw.dup.force_encoding("UTF-8") }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias readAsText read_as_text
|
|
36
|
+
|
|
37
|
+
def read_as_data_url(blob)
|
|
38
|
+
schedule_read(blob) do |raw|
|
|
39
|
+
mime = blob.respond_to?(:type) ? blob.type.to_s : ""
|
|
40
|
+
mime = "application/octet-stream" if mime.empty?
|
|
41
|
+
"data:#{mime};base64,#{[raw].pack("m0")}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias readAsDataURL read_as_data_url
|
|
46
|
+
|
|
47
|
+
def read_as_array_buffer(blob)
|
|
48
|
+
schedule_read(blob) { |raw| raw.bytes }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias readAsArrayBuffer read_as_array_buffer
|
|
52
|
+
|
|
53
|
+
def read_as_binary_string(blob)
|
|
54
|
+
schedule_read(blob) { |raw| raw.dup.force_encoding("ASCII-8BIT") }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias readAsBinaryString read_as_binary_string
|
|
58
|
+
|
|
59
|
+
def abort
|
|
60
|
+
return if @ready_state != LOADING
|
|
61
|
+
|
|
62
|
+
@aborted = true
|
|
63
|
+
@generation += 1
|
|
64
|
+
@ready_state = DONE
|
|
65
|
+
@result = nil
|
|
66
|
+
dispatch_event(Event.new("abort"))
|
|
67
|
+
dispatch_event(Event.new("loadend"))
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def __js_get__(key)
|
|
72
|
+
case key
|
|
73
|
+
when "readyState"
|
|
74
|
+
@ready_state
|
|
75
|
+
when "result"
|
|
76
|
+
@result
|
|
77
|
+
when "error"
|
|
78
|
+
@error
|
|
79
|
+
when "EMPTY"
|
|
80
|
+
EMPTY
|
|
81
|
+
when "LOADING"
|
|
82
|
+
LOADING
|
|
83
|
+
when "DONE"
|
|
84
|
+
DONE
|
|
85
|
+
else
|
|
86
|
+
@inline_handlers[inline_event_for(key)]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def __js_set__(key, value)
|
|
91
|
+
event = inline_event_for(key)
|
|
92
|
+
set_inline_handler(event, value) if event
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def __js_call__(method, args)
|
|
97
|
+
case method
|
|
98
|
+
when "readAsText"
|
|
99
|
+
read_as_text(args[0], args[1])
|
|
100
|
+
when "readAsDataURL"
|
|
101
|
+
read_as_data_url(args[0])
|
|
102
|
+
when "readAsArrayBuffer"
|
|
103
|
+
read_as_array_buffer(args[0])
|
|
104
|
+
when "readAsBinaryString"
|
|
105
|
+
read_as_binary_string(args[0])
|
|
106
|
+
when "abort"
|
|
107
|
+
abort
|
|
108
|
+
when "addEventListener"
|
|
109
|
+
add_event_listener(args[0], args[1], args[2])
|
|
110
|
+
when "removeEventListener"
|
|
111
|
+
remove_event_listener(args[0], args[1])
|
|
112
|
+
when "dispatchEvent"
|
|
113
|
+
dispatch_event(args[0])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def __internal_event_parent__
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def schedule_read(blob, &decoder)
|
|
124
|
+
@ready_state = LOADING
|
|
125
|
+
@result = nil
|
|
126
|
+
@aborted = false
|
|
127
|
+
@generation += 1
|
|
128
|
+
gen = @generation
|
|
129
|
+
dispatch_event(Event.new("loadstart"))
|
|
130
|
+
|
|
131
|
+
@window.scheduler.queue_microtask(
|
|
132
|
+
proc do
|
|
133
|
+
next unless @generation == gen && !@aborted
|
|
134
|
+
|
|
135
|
+
raw = extract_raw(blob)
|
|
136
|
+
@result = decoder.call(raw)
|
|
137
|
+
@ready_state = DONE
|
|
138
|
+
dispatch_event(Event.new("load"))
|
|
139
|
+
dispatch_event(Event.new("loadend"))
|
|
140
|
+
end
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns the blob's raw bytes as a binary String.
|
|
147
|
+
def extract_raw(blob)
|
|
148
|
+
if blob.respond_to?(:__dommy_bytes__)
|
|
149
|
+
blob.__dommy_bytes__.to_s
|
|
150
|
+
else
|
|
151
|
+
blob.to_s
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
INLINE_EVENT_MAP = INLINE_HANDLERS
|
|
156
|
+
.each_with_object({}) do |name, h|
|
|
157
|
+
h["on#{name}"] = name
|
|
158
|
+
end
|
|
159
|
+
.freeze
|
|
160
|
+
|
|
161
|
+
def inline_event_for(key)
|
|
162
|
+
INLINE_EVENT_MAP[key.to_s]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def set_inline_handler(event, handler)
|
|
166
|
+
previous = @inline_handlers[event]
|
|
167
|
+
remove_event_listener(event, previous) if previous
|
|
168
|
+
if handler.nil?
|
|
169
|
+
@inline_handlers.delete(event)
|
|
170
|
+
else
|
|
171
|
+
add_event_listener(event, handler)
|
|
172
|
+
@inline_handlers[event] = handler
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/dommy/form_data.rb
CHANGED
|
@@ -146,7 +146,7 @@ module Dommy
|
|
|
146
146
|
next if name.empty?
|
|
147
147
|
next if disabled?(el)
|
|
148
148
|
|
|
149
|
-
case el.
|
|
149
|
+
case el.__dommy_backend_node__.name
|
|
150
150
|
when "input"
|
|
151
151
|
collect_input(el, name)
|
|
152
152
|
when "select"
|
|
@@ -198,8 +198,6 @@ module Dommy
|
|
|
198
198
|
# File / Blob values pass through unchanged (multipart form
|
|
199
199
|
# encoding handles them); other values are stringified per spec.
|
|
200
200
|
return value if value.is_a?(Blob)
|
|
201
|
-
# Backward-compat: embedders' own file-marker objects.
|
|
202
|
-
return value if value.respond_to?(:__file_marker__)
|
|
203
201
|
return "" if value.nil?
|
|
204
202
|
|
|
205
203
|
value.to_s
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `window.history` polyfill. Stack-based; back/forward move the
|
|
5
|
+
# cursor. pushState appends; replaceState mutates the current entry.
|
|
6
|
+
# Each entry is `{ state:, url: }`. Popstate fires when back /
|
|
7
|
+
# forward triggers a different cursor (not on pushState per spec).
|
|
8
|
+
class History
|
|
9
|
+
def initialize(window, location)
|
|
10
|
+
@window = window
|
|
11
|
+
@location = location
|
|
12
|
+
# Initial entry mirrors the live Location. Bookmark URL is
|
|
13
|
+
# resynthesized lazily from Location each time we read it.
|
|
14
|
+
@stack = [{state: nil, url: nil}]
|
|
15
|
+
@cursor = 0
|
|
16
|
+
@scroll_restoration = "auto"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def __js_get__(key)
|
|
20
|
+
case key
|
|
21
|
+
when "length"
|
|
22
|
+
@stack.size
|
|
23
|
+
when "state"
|
|
24
|
+
@stack[@cursor][:state]
|
|
25
|
+
when "scrollRestoration"
|
|
26
|
+
@scroll_restoration
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def __js_set__(key, value)
|
|
31
|
+
case key
|
|
32
|
+
when "scrollRestoration"
|
|
33
|
+
# Per spec, only "auto" and "manual" are accepted. Invalid
|
|
34
|
+
# values silently retain the current value.
|
|
35
|
+
v = value.to_s
|
|
36
|
+
@scroll_restoration = v if %w[auto manual].include?(v)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def __js_call__(method, args)
|
|
43
|
+
case method
|
|
44
|
+
when "pushState"
|
|
45
|
+
push(args[0], args[2])
|
|
46
|
+
when "replaceState"
|
|
47
|
+
replace(args[0], args[2])
|
|
48
|
+
when "back"
|
|
49
|
+
go(-1)
|
|
50
|
+
when "forward"
|
|
51
|
+
go(1)
|
|
52
|
+
when "go"
|
|
53
|
+
go((args[0] || 0).to_i)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def push(state, url)
|
|
60
|
+
@stack = @stack[0..@cursor]
|
|
61
|
+
@location.__internal_set_url__(url.to_s) if url
|
|
62
|
+
# WHATWG: pushState serializes the state via structured-clone
|
|
63
|
+
# so subsequent caller-side mutation of the original cannot
|
|
64
|
+
# affect history.state.
|
|
65
|
+
@stack << {state: Dommy.structured_clone(state), url: nil}
|
|
66
|
+
@cursor = @stack.size - 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def replace(state, url)
|
|
70
|
+
@location.__internal_set_url__(url.to_s) if url
|
|
71
|
+
@stack[@cursor] = {state: Dommy.structured_clone(state), url: nil}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def go(delta)
|
|
75
|
+
target = @cursor + delta
|
|
76
|
+
return if target < 0 || target >= @stack.size
|
|
77
|
+
|
|
78
|
+
@cursor = target
|
|
79
|
+
@window.fire_popstate(@stack[@cursor][:state])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -46,9 +46,9 @@ module Dommy
|
|
|
46
46
|
return nil if key.empty?
|
|
47
47
|
|
|
48
48
|
to_a.find do |el|
|
|
49
|
-
next false unless el.respond_to?(:
|
|
49
|
+
next false unless el.respond_to?(:__dommy_backend_node__)
|
|
50
50
|
|
|
51
|
-
el.
|
|
51
|
+
el.__dommy_backend_node__["id"].to_s == key || el.__dommy_backend_node__["name"].to_s == key
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -125,7 +125,7 @@ module Dommy
|
|
|
125
125
|
# accepts either another option (insert before that node) or an
|
|
126
126
|
# integer index. Strings/`null` append.
|
|
127
127
|
def add(option, before = nil)
|
|
128
|
-
return nil unless option.respond_to?(:
|
|
128
|
+
return nil unless option.respond_to?(:__dommy_backend_node__)
|
|
129
129
|
|
|
130
130
|
case before
|
|
131
131
|
when nil
|
|
@@ -134,7 +134,7 @@ module Dommy
|
|
|
134
134
|
anchor = item(before)
|
|
135
135
|
anchor ? @owner.insert_before(option, anchor) : @owner.append_child(option)
|
|
136
136
|
else
|
|
137
|
-
if before.respond_to?(:
|
|
137
|
+
if before.respond_to?(:__dommy_backend_node__)
|
|
138
138
|
@owner.insert_before(option, before)
|
|
139
139
|
else
|
|
140
140
|
@owner.append_child(option)
|