phlex-reactive 0.15.3 → 0.15.4
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/CHANGELOG.md +168 -0
- data/README.md +81 -12
- data/app/javascript/phlex/reactive/core.js +1 -0
- data/app/javascript/phlex/reactive/core.min.js +6 -6
- data/app/javascript/phlex/reactive/core.min.js.map +4 -4
- data/app/javascript/phlex/reactive/features/bindings.js +129 -16
- data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
- data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/countdown.js +140 -0
- data/app/javascript/phlex/reactive/features/countdown.min.js +4 -0
- data/app/javascript/phlex/reactive/features/countdown.min.js.map +10 -0
- data/app/javascript/phlex/reactive/features/persist.js +10 -6
- data/app/javascript/phlex/reactive/features/persist.min.js +2 -2
- data/app/javascript/phlex/reactive/features/persist.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +8 -7
- data/app/javascript/phlex/reactive/runtime.js +172 -30
- data/lib/phlex/reactive/component/countdown_binding.rb +80 -0
- data/lib/phlex/reactive/component/field_references.rb +130 -0
- data/lib/phlex/reactive/component/helpers.rb +72 -14
- data/lib/phlex/reactive/component/image_fallback_binding.rb +18 -5
- data/lib/phlex/reactive/component/media_binding.rb +9 -1
- data/lib/phlex/reactive/component/media_pattern.rb +17 -17
- data/lib/phlex/reactive/component/selection.rb +24 -2
- data/lib/phlex/reactive/component/show_options.rb +91 -0
- data/lib/phlex/reactive/engine.rb +2 -2
- data/lib/phlex/reactive/js.rb +44 -2
- data/lib/phlex/reactive/show_conditions/compare_terms.rb +142 -0
- data/lib/phlex/reactive/show_conditions.rb +39 -6
- data/lib/phlex/reactive/streamable.rb +6 -3
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +8 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Reactive
|
|
5
|
+
module ShowConditions
|
|
6
|
+
# The Hash-valued terms that are not a count: `same:` (issue #395) — a
|
|
7
|
+
# field equal to ANOTHER owned field — and `digits:` (issue #396) — a
|
|
8
|
+
# closed format: ASCII digits only, an exact count or an Integer Range,
|
|
9
|
+
# and an optional digit prefix (`starts_with:`). Compiled and evaluated
|
|
10
|
+
# here; the client mirrors both in features/bindings.js, and the shared
|
|
11
|
+
# fixture (spec/fixtures/show_predicate_vectors.json) holds them to
|
|
12
|
+
# agreement.
|
|
13
|
+
#
|
|
14
|
+
# A negated term (under unless:) keeps its shape and carries
|
|
15
|
+
# "negate" => true, the way unless: turns equals into not.
|
|
16
|
+
module CompareTerms
|
|
17
|
+
# The digit-count wire keys: the count suffixes length: uses.
|
|
18
|
+
DIGITS_KEYS = %w[digits_eq digits_gte digits_lt digits_lte].freeze
|
|
19
|
+
DIGIT_STRING = /\A[0-9]*\z/
|
|
20
|
+
PREFIX = /\A[0-9]+\z/
|
|
21
|
+
|
|
22
|
+
# { digits: 10, starts_with: "9" } -> ONE term (a Range's bounds sit in
|
|
23
|
+
# the same term, so unless: can negate the whole format at once).
|
|
24
|
+
def digits_term(name, hash, negate:)
|
|
25
|
+
options = option_keys(hash)
|
|
26
|
+
if (extra = options.keys - %i[digits starts_with]).any?
|
|
27
|
+
raise ArgumentError, "reactive_show: #{name.inspect} digits: takes only starts_with: (got #{extra.inspect})"
|
|
28
|
+
end
|
|
29
|
+
unless options.key?(:digits)
|
|
30
|
+
raise ArgumentError, "reactive_show: #{name.inspect} starts_with: needs digits: (e.g. digits: 10)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
term = { "field" => name }.merge(digits_bounds(name, options[:digits]))
|
|
34
|
+
term["prefix"] = digits_prefix!(name, options[:starts_with]) if options.key?(:starts_with)
|
|
35
|
+
term["negate"] = true if negate
|
|
36
|
+
term
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# digits: — every character is 0-9, the count fits every bound, and the
|
|
40
|
+
# value starts with the prefix; negate flips it. A malformed literal
|
|
41
|
+
# (a hand-built term) is false either way, like the client's skip.
|
|
42
|
+
def digits_matches?(term, value)
|
|
43
|
+
keys = DIGITS_KEYS.select { term.key?(it) }
|
|
44
|
+
return false unless keys.all? { term[it].is_a?(Integer) } && (term["prefix"].nil? || term["prefix"].is_a?(String))
|
|
45
|
+
|
|
46
|
+
text = value.to_s
|
|
47
|
+
ok = text.match?(DIGIT_STRING) &&
|
|
48
|
+
keys.all? { count_matches?(it, term[it], text.length) } &&
|
|
49
|
+
(term["prefix"].nil? || text.start_with?(term["prefix"]))
|
|
50
|
+
ok != (term["negate"] == true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Symbol/String keys as Symbols; any other key stays as it is, so the
|
|
54
|
+
# unknown-key check names it in an ArgumentError.
|
|
55
|
+
def option_keys(hash)
|
|
56
|
+
hash.transform_keys { it.is_a?(String) || it.is_a?(Symbol) ? it.to_sym : it }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# { same: :other, case: :fold } -> one term comparing this field's
|
|
60
|
+
# value with another owned field's, both read as strings. The other
|
|
61
|
+
# name is a bare field like any show field (the client applies
|
|
62
|
+
# reactive_scope).
|
|
63
|
+
def same_term(name, hash, negate:)
|
|
64
|
+
options = option_keys(hash)
|
|
65
|
+
if (extra = options.keys - %i[same case]).any?
|
|
66
|
+
raise ArgumentError, "reactive_show: #{name.inspect} same: takes only case: (got #{extra.inspect})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
term = { "field" => name, "same" => same_field!(name, options[:same]) }
|
|
70
|
+
term["fold"] = true if same_case!(name, options[:case])
|
|
71
|
+
term["negate"] = true if negate
|
|
72
|
+
term
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# same: — this field's value against the other field's (an absent
|
|
76
|
+
# field is ""), folded when the term says so; negate flips it.
|
|
77
|
+
def same_matches?(term, value, values)
|
|
78
|
+
left = value.to_s
|
|
79
|
+
right = values.fetch(term["same"], "").to_s
|
|
80
|
+
if term["fold"] == true
|
|
81
|
+
left = ascii_fold(left)
|
|
82
|
+
right = ascii_fold(right)
|
|
83
|
+
end
|
|
84
|
+
(left == right) != (term["negate"] == true)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The ASCII-only fold both evaluators apply: A-Z to a-z and nothing
|
|
88
|
+
# else (never downcase, which folds letters the client would not).
|
|
89
|
+
def ascii_fold(value)
|
|
90
|
+
value.tr("A-Z", "a-z")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def digits_bounds(name, digits)
|
|
96
|
+
case digits
|
|
97
|
+
when Integer
|
|
98
|
+
return { "digits_eq" => digits } if digits >= 0
|
|
99
|
+
when Range
|
|
100
|
+
first = digits.begin
|
|
101
|
+
last = digits.end
|
|
102
|
+
if [first, last].compact.any? && [first, last].compact.all? { it.is_a?(Integer) && it >= 0 }
|
|
103
|
+
bounds = {}
|
|
104
|
+
bounds["digits_gte"] = first unless first.nil?
|
|
105
|
+
bounds[digits.exclude_end? ? "digits_lt" : "digits_lte"] = last unless last.nil?
|
|
106
|
+
return bounds
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"reactive_show: #{name.inspect} digits: takes a non-negative Integer or an Integer Range " \
|
|
112
|
+
"(a digit count), got #{digits.inspect}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def digits_prefix!(name, prefix)
|
|
116
|
+
return prefix if prefix.is_a?(String) && prefix.match?(PREFIX)
|
|
117
|
+
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"reactive_show: #{name.inspect} starts_with: takes a String of digits (0-9), got #{prefix.inspect}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def same_field!(name, other)
|
|
123
|
+
field = other.to_s if other.is_a?(Symbol) || other.is_a?(String)
|
|
124
|
+
return field unless field.nil? || field.strip.empty?
|
|
125
|
+
|
|
126
|
+
raise ArgumentError,
|
|
127
|
+
"reactive_show: #{name.inspect} same: needs the other field's name (e.g. same: :email), " \
|
|
128
|
+
"got #{other.inspect}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# case: is absent (an exact compare) or :fold. Anything else raises:
|
|
132
|
+
# there is no locale-aware fold to ask for.
|
|
133
|
+
def same_case!(name, value)
|
|
134
|
+
return false if value.nil?
|
|
135
|
+
return true if (value.is_a?(Symbol) || value.is_a?(String)) && value.to_s == "fold"
|
|
136
|
+
|
|
137
|
+
raise ArgumentError, "reactive_show: #{name.inspect} case: takes only :fold, got #{value.inspect}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -23,6 +23,9 @@ module Phlex
|
|
|
23
23
|
# { checked: 1.. } / { checked: 0 } -> checked_* (issue #319 — how many
|
|
24
24
|
# owned boxes of a checkbox GROUP are
|
|
25
25
|
# ticked; same Integer/Range shapes)
|
|
26
|
+
# { same: :email } -> same "email" (issue #395 — equal to
|
|
27
|
+
# { same: :email, case: :fold } ANOTHER owned field; fold lowercases
|
|
28
|
+
# ASCII A-Z only, on both sides)
|
|
26
29
|
# Under unless: each term is NEGATED (De Morgan):
|
|
27
30
|
# scalar -> not; Array -> ∉ (AND of nots); Range -> the complement
|
|
28
31
|
# predicate (¬gte→lt, ¬lte→gt, ¬lt→gte), and a BOUNDED range complements
|
|
@@ -49,6 +52,8 @@ module Phlex
|
|
|
49
52
|
# A Hash value's one key -> its wire-key prefix.
|
|
50
53
|
COUNT_PREFIXES = { "length" => "len", "checked" => "checked" }.freeze
|
|
51
54
|
|
|
55
|
+
extend CompareTerms
|
|
56
|
+
|
|
52
57
|
module_function
|
|
53
58
|
|
|
54
59
|
# Compile if:/if_any:/unless: into DNF groups. Loud validation at render
|
|
@@ -83,7 +88,7 @@ module Phlex
|
|
|
83
88
|
# Every field named across all groups/terms — drives reactive_values
|
|
84
89
|
# coverage (server first paint only fires when every field is provided).
|
|
85
90
|
def fields(groups)
|
|
86
|
-
groups.flat_map { |group| group.
|
|
91
|
+
groups.flat_map { |group| group.flat_map { |term| [term.fetch("field"), term["same"]].compact } }.uniq
|
|
87
92
|
end
|
|
88
93
|
# rubocop:enable Style/ItBlockParameter
|
|
89
94
|
|
|
@@ -147,7 +152,7 @@ module Phlex
|
|
|
147
152
|
case value
|
|
148
153
|
when Range then range_terms(name, value)
|
|
149
154
|
when Array then [membership_term(name, value)]
|
|
150
|
-
when Hash then
|
|
155
|
+
when Hash then hash_terms(name, value)
|
|
151
156
|
else [{ "field" => name, "equals" => equals_literal(value) }]
|
|
152
157
|
end
|
|
153
158
|
end
|
|
@@ -191,6 +196,32 @@ module Phlex
|
|
|
191
196
|
end
|
|
192
197
|
end
|
|
193
198
|
|
|
199
|
+
# --- Hash values: same: (#395, in CompareTerms) or a count -------------
|
|
200
|
+
|
|
201
|
+
def hash_terms(name, hash)
|
|
202
|
+
case hash_kind(hash)
|
|
203
|
+
when :same then [same_term(name, hash, negate: false)]
|
|
204
|
+
when :digits then [digits_term(name, hash, negate: false)]
|
|
205
|
+
else count_terms(name, hash)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The complement under unless: — a list of alternative term-lists.
|
|
210
|
+
def hash_complement(name, hash)
|
|
211
|
+
case hash_kind(hash)
|
|
212
|
+
when :same then [[same_term(name, hash, negate: true)]]
|
|
213
|
+
when :digits then [[digits_term(name, hash, negate: true)]]
|
|
214
|
+
else count_complement(name, hash)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def hash_kind(hash)
|
|
219
|
+
keys = hash.keys.map(&:to_s)
|
|
220
|
+
if keys.include?("same") then :same
|
|
221
|
+
elsif keys.include?("digits") || keys.include?("starts_with") then :digits
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
194
225
|
# --- the count predicates: length: (#226) and checked: (#319) ---------
|
|
195
226
|
|
|
196
227
|
# A Hash value names a STRUCTURAL predicate over a COUNT — length: (the
|
|
@@ -262,7 +293,7 @@ module Phlex
|
|
|
262
293
|
kind = hash.keys.first.to_s if hash.size == 1
|
|
263
294
|
unless COUNT_PREFIXES.key?(kind)
|
|
264
295
|
raise ArgumentError,
|
|
265
|
-
"reactive_show: #{name.inspect} Hash value supports only length: or checked: " \
|
|
296
|
+
"reactive_show: #{name.inspect} Hash value supports only same:, digits:, or one of length: or checked: " \
|
|
266
297
|
"(got #{hash.keys.inspect})"
|
|
267
298
|
end
|
|
268
299
|
|
|
@@ -294,7 +325,7 @@ module Phlex
|
|
|
294
325
|
name = field.to_s
|
|
295
326
|
case value
|
|
296
327
|
when Range then range_complement(name, value)
|
|
297
|
-
when Hash then
|
|
328
|
+
when Hash then hash_complement(name, value)
|
|
298
329
|
when Array
|
|
299
330
|
list = value.map(&:to_s)
|
|
300
331
|
raise ArgumentError, "reactive_show: #{name.inspect} Array needs at least one value" if list.empty?
|
|
@@ -333,7 +364,9 @@ module Phlex
|
|
|
333
364
|
field = term.fetch("field")
|
|
334
365
|
value = values.fetch(field, "")
|
|
335
366
|
|
|
336
|
-
if term.key?("
|
|
367
|
+
if term.key?("same") then same_matches?(term, value, values)
|
|
368
|
+
elsif CompareTerms::DIGITS_KEYS.any? { term.key?(it) } then digits_matches?(term, value)
|
|
369
|
+
elsif term.key?("equals") then value == term["equals"]
|
|
337
370
|
elsif term.key?("not") then value != term["not"]
|
|
338
371
|
elsif term.key?("in") then term["in"].include?(value)
|
|
339
372
|
elsif (key = LENGTH_KEYS.find { term.key?(it) }) then count_matches?(key, term[key], value.to_s.length)
|
|
@@ -350,7 +383,7 @@ module Phlex
|
|
|
350
383
|
def count_matches?(key, literal, count)
|
|
351
384
|
return false unless literal.is_a?(Integer)
|
|
352
385
|
|
|
353
|
-
case key.
|
|
386
|
+
case key.split("_", 2).last
|
|
354
387
|
when "eq" then count == literal
|
|
355
388
|
when "gte" then count >= literal
|
|
356
389
|
when "gt" then count > literal
|
|
@@ -34,7 +34,9 @@ module Phlex
|
|
|
34
34
|
ThreadViewContext = Struct.new(:view_context, :builder, :renderer, :generation)
|
|
35
35
|
|
|
36
36
|
# Actor-only ops: a js broadcast rejects a broadcast that carries one.
|
|
37
|
-
# Focus ops steal focus in every subscriber's tab (issue #96);
|
|
37
|
+
# Focus ops steal focus in every subscriber's tab (issue #96); blur (issue
|
|
38
|
+
# #399) would drop focus in every subscriber whose focused element sits
|
|
39
|
+
# inside the target; submit
|
|
38
40
|
# (issue #226) would force-submit every subscriber's form; paste_into
|
|
39
41
|
# (issue #228) would read every subscriber's clipboard; check_group
|
|
40
42
|
# (issue #342) would tick or clear every subscriber's selection; presence
|
|
@@ -46,7 +48,7 @@ module Phlex
|
|
|
46
48
|
# belong to the actor's own reply (reply.js) or gesture (on_client / a
|
|
47
49
|
# reducer's $ops), never a broadcast. Names mirror Phlex::Reactive::JS's verbs.
|
|
48
50
|
BROADCAST_REFUSED_OPS = %w[
|
|
49
|
-
focus focus_first submit paste_into persist_state persist_clear check_group presence
|
|
51
|
+
focus focus_first blur submit paste_into persist_state persist_clear check_group presence
|
|
50
52
|
show_modal close_dialog
|
|
51
53
|
preview_file
|
|
52
54
|
set_value
|
|
@@ -205,7 +207,8 @@ module Phlex
|
|
|
205
207
|
unless refused.empty?
|
|
206
208
|
raise ArgumentError,
|
|
207
209
|
"broadcast_to(js:) refuses actor-only op(s) #{refused.join(", ")} — broadcasting focus " \
|
|
208
|
-
"steals it in every subscriber's tab; broadcasting
|
|
210
|
+
"steals it in every subscriber's tab; broadcasting blur drops focus in every subscriber " \
|
|
211
|
+
"whose focused element is inside the target; broadcasting submit force-submits every " \
|
|
209
212
|
"subscriber's form; broadcasting paste_into reads every subscriber's clipboard; " \
|
|
210
213
|
"broadcasting check_group ticks or clears every subscriber's selection; " \
|
|
211
214
|
"broadcasting presence writes every subscriber's presence state; " \
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-reactive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.15.
|
|
4
|
+
version: 0.15.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -140,6 +140,9 @@ files:
|
|
|
140
140
|
- app/javascript/phlex/reactive/features/compute.js
|
|
141
141
|
- app/javascript/phlex/reactive/features/compute.min.js
|
|
142
142
|
- app/javascript/phlex/reactive/features/compute.min.js.map
|
|
143
|
+
- app/javascript/phlex/reactive/features/countdown.js
|
|
144
|
+
- app/javascript/phlex/reactive/features/countdown.min.js
|
|
145
|
+
- app/javascript/phlex/reactive/features/countdown.min.js.map
|
|
143
146
|
- app/javascript/phlex/reactive/features/defer.js
|
|
144
147
|
- app/javascript/phlex/reactive/features/defer.min.js
|
|
145
148
|
- app/javascript/phlex/reactive/features/defer.min.js.map
|
|
@@ -193,8 +196,10 @@ files:
|
|
|
193
196
|
- lib/phlex/reactive/client_bindings.rb
|
|
194
197
|
- lib/phlex/reactive/collections.rb
|
|
195
198
|
- lib/phlex/reactive/component.rb
|
|
199
|
+
- lib/phlex/reactive/component/countdown_binding.rb
|
|
196
200
|
- lib/phlex/reactive/component/dsl.rb
|
|
197
201
|
- lib/phlex/reactive/component/editable.rb
|
|
202
|
+
- lib/phlex/reactive/component/field_references.rb
|
|
198
203
|
- lib/phlex/reactive/component/helpers.rb
|
|
199
204
|
- lib/phlex/reactive/component/identity.rb
|
|
200
205
|
- lib/phlex/reactive/component/image_fallback_binding.rb
|
|
@@ -204,6 +209,7 @@ files:
|
|
|
204
209
|
- lib/phlex/reactive/component/presence_declaration.rb
|
|
205
210
|
- lib/phlex/reactive/component/registry.rb
|
|
206
211
|
- lib/phlex/reactive/component/selection.rb
|
|
212
|
+
- lib/phlex/reactive/component/show_options.rb
|
|
207
213
|
- lib/phlex/reactive/component/sortable.rb
|
|
208
214
|
- lib/phlex/reactive/defer.rb
|
|
209
215
|
- lib/phlex/reactive/deferred_render_job.rb
|
|
@@ -241,6 +247,7 @@ files:
|
|
|
241
247
|
- lib/phlex/reactive/settles.rb
|
|
242
248
|
- lib/phlex/reactive/shortcut.rb
|
|
243
249
|
- lib/phlex/reactive/show_conditions.rb
|
|
250
|
+
- lib/phlex/reactive/show_conditions/compare_terms.rb
|
|
244
251
|
- lib/phlex/reactive/stream.rb
|
|
245
252
|
- lib/phlex/reactive/streamable.rb
|
|
246
253
|
- lib/phlex/reactive/test_helpers.rb
|