phlex-reactive 0.14.4 → 0.15.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/README.md +191 -1
  4. data/app/controllers/phlex/reactive/actions_controller.rb +28 -18
  5. data/app/javascript/phlex/reactive/core.js +5 -5
  6. data/app/javascript/phlex/reactive/core.min.js +3 -3
  7. data/app/javascript/phlex/reactive/core.min.js.map +4 -4
  8. data/app/javascript/phlex/reactive/early.js +28 -9
  9. data/app/javascript/phlex/reactive/early.min.js +2 -2
  10. data/app/javascript/phlex/reactive/early.min.js.map +3 -3
  11. data/app/javascript/phlex/reactive/features/bindings.js +125 -140
  12. data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
  13. data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
  14. data/app/javascript/phlex/reactive/features/form.js +63 -41
  15. data/app/javascript/phlex/reactive/features/form.min.js +2 -2
  16. data/app/javascript/phlex/reactive/features/form.min.js.map +3 -3
  17. data/app/javascript/phlex/reactive/features/presence.js +205 -0
  18. data/app/javascript/phlex/reactive/features/presence.min.js +4 -0
  19. data/app/javascript/phlex/reactive/features/presence.min.js.map +10 -0
  20. data/app/javascript/phlex/reactive/inspect.js +5 -1
  21. data/app/javascript/phlex/reactive/inspect.min.js +2 -2
  22. data/app/javascript/phlex/reactive/inspect.min.js.map +3 -3
  23. data/app/javascript/phlex/reactive/reactive_controller.js +3 -1
  24. data/app/javascript/phlex/reactive/reactive_controller.min.js +3 -3
  25. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +8 -7
  26. data/app/javascript/phlex/reactive/runtime.js +175 -14
  27. data/lib/phlex/reactive/component/dsl.rb +5 -0
  28. data/lib/phlex/reactive/component/helpers.rb +79 -8
  29. data/lib/phlex/reactive/component/presence_declaration.rb +118 -0
  30. data/lib/phlex/reactive/component/registry.rb +3 -1
  31. data/lib/phlex/reactive/component.rb +1 -0
  32. data/lib/phlex/reactive/defer.rb +5 -0
  33. data/lib/phlex/reactive/engine.rb +9 -4
  34. data/lib/phlex/reactive/js.rb +24 -0
  35. data/lib/phlex/reactive/presence/component_methods.rb +130 -0
  36. data/lib/phlex/reactive/presence/roster.rb +90 -0
  37. data/lib/phlex/reactive/presence/transport.rb +154 -0
  38. data/lib/phlex/reactive/presence.rb +162 -0
  39. data/lib/phlex/reactive/presence_render_job.rb +26 -0
  40. data/lib/phlex/reactive/shortcut.rb +105 -0
  41. data/lib/phlex/reactive/streamable.rb +7 -3
  42. data/lib/phlex/reactive/version.rb +1 -1
  43. data/lib/phlex/reactive.rb +10 -0
  44. metadata +11 -1
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Reactive
5
+ # A page-level keyboard shortcut (issue #357), the value behind
6
+ # `on(..., shortcut:)` and `on_client(:keydown, ..., shortcut:)`.
7
+ #
8
+ # Shortcut.parse("mod+z") # String form
9
+ # Shortcut.parse({ keys: "esc", in_editable: true }) # Hash form
10
+ #
11
+ # `mod` is meta on Apple platforms and ctrl elsewhere. Stimulus has no
12
+ # "either" modifier (its key filter compares all four modifier flags
13
+ # exactly), so a `mod` chord renders TWO descriptors, a meta twin and a ctrl
14
+ # twin, and the client runs only the twin for its platform. Stimulus keeps
15
+ # the key filtering; the client adds the platform check, the editable-target
16
+ # guard and preventDefault.
17
+ #
18
+ # Validation is loud, at render: Stimulus reports an unknown key token by
19
+ # throwing at keypress time, which is too late to notice.
20
+ class Shortcut
21
+ MODIFIERS = %w[mod meta ctrl alt shift].freeze
22
+ # Stimulus's default keyMappings: the named keys plus a-z and 0-9.
23
+ NAMED_KEYS = %w[enter tab esc space up down left right home end page_up page_down].freeze
24
+ HASH_KEYS = %i[keys in_editable].freeze
25
+
26
+ private_constant :MODIFIERS, :NAMED_KEYS, :HASH_KEYS
27
+
28
+ attr_reader :keys
29
+
30
+ def self.parse(spec)
31
+ case spec
32
+ when String then new(spec, in_editable: false)
33
+ when Hash then from_hash(spec)
34
+ else
35
+ raise ArgumentError,
36
+ "shortcut: takes a String (\"mod+z\") or a Hash ({ keys: \"esc\", in_editable: true }), " \
37
+ "got #{spec.inspect}"
38
+ end
39
+ end
40
+
41
+ def self.from_hash(spec)
42
+ keys = spec[:keys]
43
+ in_editable = spec.fetch(:in_editable, false)
44
+ unless (spec.keys - HASH_KEYS).empty? && keys.is_a?(String)
45
+ raise ArgumentError,
46
+ "shortcut: as a Hash takes keys: (a String, e.g. \"esc\") and an optional " \
47
+ "in_editable: (true/false), got #{spec.inspect}"
48
+ end
49
+ unless [true, false].include?(in_editable)
50
+ raise ArgumentError, "shortcut: in_editable: is true or false, got #{in_editable.inspect}"
51
+ end
52
+
53
+ new(keys, in_editable:)
54
+ end
55
+ private_class_method :from_hash
56
+
57
+ def initialize(keys, in_editable:)
58
+ tokens = keys.downcase.split("+").map(&:strip)
59
+ @in_editable = in_editable
60
+ @keys = canonical(keys, tokens).freeze
61
+ freeze
62
+ end
63
+
64
+ # The Stimulus key filters to bind: the meta and ctrl twins for a `mod`
65
+ # chord, else the chord itself.
66
+ def filters
67
+ return [@keys] unless @keys.start_with?("mod+")
68
+
69
+ %w[meta ctrl].map { @keys.sub("mod", it) }
70
+ end
71
+
72
+ # The wire form the client reads (data-reactive-shortcut-param, or the
73
+ # on_client record's "shortcut"): the canonical String, or the Hash form
74
+ # when the shortcut also fires inside editable targets.
75
+ def param = @in_editable ? { "keys" => @keys, "inEditable" => true } : @keys
76
+
77
+ private
78
+
79
+ def canonical(source, tokens)
80
+ mods = tokens.select { MODIFIERS.include?(it) }
81
+ key = tokens - MODIFIERS
82
+ validate!(source, mods, key)
83
+ [*MODIFIERS.select { mods.include?(it) }, key.first].join("+")
84
+ end
85
+
86
+ def validate!(source, mods, key)
87
+ raise ArgumentError, "shortcut: #{source.inspect} must name exactly one key (e.g. \"mod+z\")" unless key.size == 1
88
+ raise ArgumentError, "shortcut: #{source.inspect} repeats a modifier" unless mods.uniq.size == mods.size
89
+ if mods.include?("mod") && mods.intersect?(%w[meta ctrl])
90
+ raise ArgumentError,
91
+ "shortcut: #{source.inspect} combines mod with meta/ctrl — mod already is meta on Apple " \
92
+ "platforms and ctrl elsewhere"
93
+ end
94
+ return if known_key?(key.first)
95
+
96
+ raise ArgumentError,
97
+ "shortcut: #{source.inspect} has an unknown key #{key.first.inspect} — use a letter, a digit, " \
98
+ "or one of #{NAMED_KEYS.join(", ")} (Stimulus's default key map; custom keyMappings are not " \
99
+ "known on the server, bind those with event: and window: instead)"
100
+ end
101
+
102
+ def known_key?(key) = NAMED_KEYS.include?(key) || key.match?(/\A[a-z0-9]\z/)
103
+ end
104
+ end
105
+ end
@@ -37,10 +37,13 @@ module Phlex
37
37
  # Focus ops steal focus in every subscriber's tab (issue #96); submit
38
38
  # (issue #226) would force-submit every subscriber's form; paste_into
39
39
  # (issue #228) would read every subscriber's clipboard; check_group
40
- # (issue #342) would tick or clear every subscriber's selection. They
40
+ # (issue #342) would tick or clear every subscriber's selection; presence
41
+ # (issue #358) would write every subscriber's presence state. They
41
42
  # belong to the actor's own reply (reply.js) or gesture (on_client / a
42
43
  # reducer's $ops), never a broadcast. Names mirror Phlex::Reactive::JS's verbs.
43
- BROADCAST_REFUSED_OPS = %w[focus focus_first submit paste_into persist_state persist_clear check_group].freeze
44
+ BROADCAST_REFUSED_OPS = %w[
45
+ focus focus_first submit paste_into persist_state persist_clear check_group presence
46
+ ].freeze
44
47
 
45
48
  # The broadcast_to verb kwargs (issue #185) → their Turbo stream action.
46
49
  # SELF-TARGETING verbs derive the target from the component's #id (require a
@@ -193,7 +196,8 @@ module Phlex
193
196
  "broadcast_to(js:) refuses actor-only op(s) #{refused.join(", ")} — broadcasting focus " \
194
197
  "steals it in every subscriber's tab; broadcasting submit force-submits every " \
195
198
  "subscriber's form; broadcasting paste_into reads every subscriber's clipboard; " \
196
- "broadcasting check_group ticks or clears every subscriber's selection. " \
199
+ "broadcasting check_group ticks or clears every subscriber's selection; " \
200
+ "broadcasting presence writes every subscriber's presence state. " \
197
201
  "These are actor concerns (reply.js / on_client / $ops)."
198
202
  end
199
203
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.14.4"
5
+ VERSION = "0.15.0"
6
6
  end
7
7
  end
@@ -66,6 +66,12 @@ module Phlex
66
66
  # authorization method / mark_authorized! / declare skip_verify_authorized).
67
67
  class AuthorizationNotVerified < Error; end
68
68
 
69
+ # Raised when a feature needs a transport capability the app does not have
70
+ # (issue #358: reactive_presence without a pgbus that keys presence by
71
+ # connection). Loud at first render, with the requirement in the message —
72
+ # there is no degraded mode to fall back to.
73
+ class ConfigurationError < Error; end
74
+
69
75
  # Purpose string bound into every identity token's signature so a token
70
76
  # minted for phlex-reactive can't be replayed against another verifier use.
71
77
  IDENTITY_PURPOSE = "phlex-reactive/identity"
@@ -1283,6 +1289,10 @@ loader.do_not_eager_load("#{__dir__}/reactive/engine.rb")
1283
1289
  # boot. The constant autoloads on first reference, which only happens behind
1284
1290
  # Phlex::Reactive.defer_push_capable? (that gate requires ActiveJob::Base).
1285
1291
  loader.do_not_eager_load("#{__dir__}/reactive/deferred_render_job.rb")
1292
+ # The presence roster's render job (issue #358), for the same reason: it is
1293
+ # only referenced behind Phlex::Reactive::Presence.capable? (which requires
1294
+ # ActiveJob::Base).
1295
+ loader.do_not_eager_load("#{__dir__}/reactive/presence_render_job.rb")
1286
1296
  loader.setup
1287
1297
 
1288
1298
  require "phlex/reactive/engine" if defined?(Rails::Engine)
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.14.4
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -158,6 +158,9 @@ files:
158
158
  - app/javascript/phlex/reactive/features/persist.js
159
159
  - app/javascript/phlex/reactive/features/persist.min.js
160
160
  - app/javascript/phlex/reactive/features/persist.min.js.map
161
+ - app/javascript/phlex/reactive/features/presence.js
162
+ - app/javascript/phlex/reactive/features/presence.min.js
163
+ - app/javascript/phlex/reactive/features/presence.min.js.map
161
164
  - app/javascript/phlex/reactive/features/sortable.js
162
165
  - app/javascript/phlex/reactive/features/sortable.min.js
163
166
  - app/javascript/phlex/reactive/features/sortable.min.js.map
@@ -195,6 +198,7 @@ files:
195
198
  - lib/phlex/reactive/component/helpers.rb
196
199
  - lib/phlex/reactive/component/identity.rb
197
200
  - lib/phlex/reactive/component/lazy.rb
201
+ - lib/phlex/reactive/component/presence_declaration.rb
198
202
  - lib/phlex/reactive/component/registry.rb
199
203
  - lib/phlex/reactive/component/selection.rb
200
204
  - lib/phlex/reactive/component/sortable.rb
@@ -222,10 +226,16 @@ files:
222
226
  - lib/phlex/reactive/param_schema.rb
223
227
  - lib/phlex/reactive/pending.rb
224
228
  - lib/phlex/reactive/pending/markup.rb
229
+ - lib/phlex/reactive/presence.rb
230
+ - lib/phlex/reactive/presence/component_methods.rb
231
+ - lib/phlex/reactive/presence/roster.rb
232
+ - lib/phlex/reactive/presence/transport.rb
233
+ - lib/phlex/reactive/presence_render_job.rb
225
234
  - lib/phlex/reactive/reply.rb
226
235
  - lib/phlex/reactive/response.rb
227
236
  - lib/phlex/reactive/settle.rb
228
237
  - lib/phlex/reactive/settles.rb
238
+ - lib/phlex/reactive/shortcut.rb
229
239
  - lib/phlex/reactive/show_conditions.rb
230
240
  - lib/phlex/reactive/stream.rb
231
241
  - lib/phlex/reactive/streamable.rb