phlex-reactive 0.16.5 → 0.16.7
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 +77 -1
- data/README.md +96 -5
- data/app/javascript/phlex/reactive/core.min.js +6 -6
- data/app/javascript/phlex/reactive/core.min.js.map +3 -3
- data/app/javascript/phlex/reactive/early.js +16 -1
- data/app/javascript/phlex/reactive/early.min.js +2 -2
- data/app/javascript/phlex/reactive/early.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/app/javascript/phlex/reactive/runtime.js +73 -23
- data/lib/phlex/reactive/component/helpers.rb +43 -4
- data/lib/phlex/reactive/component/shared_field.rb +40 -0
- data/lib/phlex/reactive/js.rb +25 -2
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +2 -1
|
@@ -1490,16 +1490,36 @@ function previewFile(input, args, resolveTargets) {
|
|
|
1490
1490
|
// `invalid` event fired into the app's own listeners.
|
|
1491
1491
|
const files = [...(args.valid === true && input.validity?.valid === false ? [] : (input.files ?? []))]
|
|
1492
1492
|
const accepts = previewAccepts(input, args.accept)
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1493
|
+
// Size is read BEFORE an object URL exists, so an over-size file never gets one.
|
|
1494
|
+
const oversize = (f) => Number.isInteger(args.max_bytes) && f.size > args.max_bytes
|
|
1495
|
+
const rejected = []
|
|
1496
|
+
let file
|
|
1497
|
+
if (args.per_file === true && input.multiple) {
|
|
1498
|
+
// Several files (issue #439): every file is checked, accept then
|
|
1499
|
+
// max_bytes. The refused ones leave input.files here, synchronously, so an
|
|
1500
|
+
// action bound after the op on the same event posts only the kept ones.
|
|
1501
|
+
const kept = files.filter((f, index) => {
|
|
1502
|
+
const reason = typeof f?.type === "string" && accepts(f) ? oversize(f) && "size" : "type"
|
|
1503
|
+
if (reason) rejected.push({ reason, file: f, index })
|
|
1504
|
+
return !reason
|
|
1505
|
+
})
|
|
1506
|
+
// remaining: the files the input keeps, and so the ones an action posts.
|
|
1507
|
+
const remaining = rejected.length > 0 && !keepFiles(input, kept) ? files.length : kept.length
|
|
1508
|
+
for (const detail of rejected) detail.remaining = remaining
|
|
1509
|
+
file = kept.find((f) => f.type.startsWith("image/"))
|
|
1510
|
+
} else {
|
|
1511
|
+
const listed = files.filter((f) => typeof f?.type === "string" && accepts(f))
|
|
1512
|
+
// Only an image is painted: a listed PDF clears the preview, unrefused.
|
|
1513
|
+
file = listed.find((f) => f.type.startsWith("image/"))
|
|
1514
|
+
// A refused file (issue #391) clears like an empty pick and says why. The
|
|
1515
|
+
// input keeps its files and its validity: the page owns the reset.
|
|
1516
|
+
let reason = files.length > 0 && listed.length === 0 ? "type" : null
|
|
1517
|
+
if (file && oversize(file)) reason = "size"
|
|
1518
|
+
if (reason) {
|
|
1519
|
+
rejected.push({ reason, file: reason === "size" ? file : files[0] })
|
|
1520
|
+
file = null
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1503
1523
|
forgetPreview(img)
|
|
1504
1524
|
if (file) {
|
|
1505
1525
|
const url = URL.createObjectURL(file)
|
|
@@ -1516,9 +1536,23 @@ function previewFile(input, args, resolveTargets) {
|
|
|
1516
1536
|
if (args.name != null) {
|
|
1517
1537
|
for (const node of resolveTargets({ ...args, to: args.name })) node.textContent = file ? file.name : ""
|
|
1518
1538
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1539
|
+
for (const detail of rejected) input.dispatchEvent(new CustomEvent("reactive:file-rejected", { bubbles: true, detail }))
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Rebuild input.files from the kept files (issue #439); true once it did. A
|
|
1543
|
+
// DataTransfer is the one way to make a FileList; without one the refused
|
|
1544
|
+
// files stay in the input (the rejections still fire), and a verbose root is told.
|
|
1545
|
+
function keepFiles(input, kept) {
|
|
1546
|
+
if (typeof DataTransfer !== "function") {
|
|
1547
|
+
if (input.closest?.('[data-reactive-verbose="true"], [data-reactive-debug="true"]')) {
|
|
1548
|
+
warnPreview("per_file: has no DataTransfer to drop the refused files from the input")
|
|
1549
|
+
}
|
|
1550
|
+
return false
|
|
1521
1551
|
}
|
|
1552
|
+
const transfer = new DataTransfer()
|
|
1553
|
+
for (const f of kept) transfer.items.add(f)
|
|
1554
|
+
input.files = transfer.files
|
|
1555
|
+
return true
|
|
1522
1556
|
}
|
|
1523
1557
|
|
|
1524
1558
|
// Which files the page takes: an image by default. accept: true reads the
|
|
@@ -3223,7 +3257,7 @@ export default class extends Controller {
|
|
|
3223
3257
|
// modifier params (issue #80). The client decides preventDefault behavior
|
|
3224
3258
|
// from event.params — set by the Ruby on() — never by sniffing the
|
|
3225
3259
|
// Stimulus descriptor.
|
|
3226
|
-
const { action, params, debounce, throttle, confirm, confirmWhen, outside, window: windowBound, optimistic } =
|
|
3260
|
+
const { action, params, debounce, throttle, confirm, confirmWhen, outside, window: windowBound, optimistic, prevent } =
|
|
3227
3261
|
event.params
|
|
3228
3262
|
if (!action) return
|
|
3229
3263
|
|
|
@@ -3276,7 +3310,11 @@ export default class extends Controller {
|
|
|
3276
3310
|
// A shortcut (issue #357) IS prevented: its key filter has already narrowed
|
|
3277
3311
|
// the window binding to one chord the page claims (mod+s must not also open
|
|
3278
3312
|
// the browser's save dialog).
|
|
3279
|
-
|
|
3313
|
+
//
|
|
3314
|
+
// `prevent: false` (issue #433) opts an element-bound trigger out too: the
|
|
3315
|
+
// event keeps its native default (a drop still reaches a file input). A
|
|
3316
|
+
// shortcut stays prevented whatever the markup says.
|
|
3317
|
+
if ((shortcut != null || (!windowBound && prevent !== false)) && !this.#keepsNativeToggle(optimistic, target))
|
|
3280
3318
|
preventTrigger(event, shortcut)
|
|
3281
3319
|
|
|
3282
3320
|
// A reactive_lazy(on:) shell's trigger (issue #276) goes through the one
|
|
@@ -3395,12 +3433,12 @@ export default class extends Controller {
|
|
|
3395
3433
|
if (controller) return controller.dispatch(event)
|
|
3396
3434
|
// Held past this event's dispatch: cancel its native default NOW, by
|
|
3397
3435
|
// dispatch()'s own rule, or a submit/link would act before the request.
|
|
3398
|
-
const { shortcut, window: windowBound, optimistic } = event.params
|
|
3436
|
+
const { shortcut, window: windowBound, optimistic, prevent } = event.params
|
|
3399
3437
|
// A shortcut that does not apply (the other twin, a field, a key handled
|
|
3400
3438
|
// first) is neither prevented nor held, as dispatch() would leave it.
|
|
3401
3439
|
if (shortcut != null && !shortcutApplies(shortcut, event, this.application?.schema?.keyMappings)) return
|
|
3402
3440
|
const target = event.currentTarget ?? event.target
|
|
3403
|
-
if ((shortcut != null || !windowBound) && !this.#keepsNativeToggle(optimistic, target))
|
|
3441
|
+
if ((shortcut != null || (!windowBound && prevent !== false)) && !this.#keepsNativeToggle(optimistic, target))
|
|
3404
3442
|
preventTrigger(event, shortcut)
|
|
3405
3443
|
heldForwards.set(root, [...(heldForwards.get(root) ?? []), [event, target]])
|
|
3406
3444
|
const data = root.dataset
|
|
@@ -3463,9 +3501,10 @@ export default class extends Controller {
|
|
|
3463
3501
|
continue
|
|
3464
3502
|
if (onceBindingSpent(this, event, record)) continue
|
|
3465
3503
|
// A legacy attr reads its flags element-wide — but never a mix-ed on()'s
|
|
3466
|
-
// debounce/throttle
|
|
3504
|
+
// debounce/throttle (issue #393) or prevent (issue #433) params: those
|
|
3505
|
+
// belong to the dispatch.
|
|
3467
3506
|
this.#runBinding(
|
|
3468
|
-
record.legacy ? { ...params, ops: record.ops, debounce: undefined, throttle: undefined } : record,
|
|
3507
|
+
record.legacy ? { ...params, ops: record.ops, debounce: undefined, throttle: undefined, prevent: undefined } : record,
|
|
3469
3508
|
event,
|
|
3470
3509
|
trigger,
|
|
3471
3510
|
records.indexOf(record),
|
|
@@ -3476,7 +3515,7 @@ export default class extends Controller {
|
|
|
3476
3515
|
// One on_client binding (issue #95): its outside guard, preventDefault rule,
|
|
3477
3516
|
// confirm gate, then its ops — every flag read from the binding itself.
|
|
3478
3517
|
#runBinding(record, event, trigger, ordinal) {
|
|
3479
|
-
const { ops, confirm, confirmWhen, outside, window: windowBound, shortcut } = record
|
|
3518
|
+
const { ops, confirm, confirmWhen, outside, window: windowBound, shortcut, prevent } = record
|
|
3480
3519
|
// Outside guard FIRST — identical semantics to dispatch() (issue #80): an
|
|
3481
3520
|
// outside: trigger is a COMPLETE no-op for events inside this root, before
|
|
3482
3521
|
// preventDefault and before any op runs.
|
|
@@ -3489,7 +3528,8 @@ export default class extends Controller {
|
|
|
3489
3528
|
// (possibly async) confirm gate below — a native default can't wait for a
|
|
3490
3529
|
// pending dialog (same ordering as dispatch()).
|
|
3491
3530
|
// A shortcut (issue #357) is prevented: it is one chord the page claims.
|
|
3492
|
-
|
|
3531
|
+
// A record with `prevent: false` (issue #433) keeps the native default.
|
|
3532
|
+
if (shortcut != null || (!windowBound && prevent !== false)) preventTrigger(event, shortcut)
|
|
3493
3533
|
|
|
3494
3534
|
// Resolve the effective confirm message — static string, or the conditional
|
|
3495
3535
|
// Hash form (issue #179) evaluated over collected fields (a promise of it
|
|
@@ -4352,12 +4392,22 @@ export default class extends Controller {
|
|
|
4352
4392
|
// #ownershipFilter for SERVER params (issue #410): nested client-only roots
|
|
4353
4393
|
// are transparent, so a control under one is this root's when the first
|
|
4354
4394
|
// token-bearing root above it is this.element. A nested token-bearing root
|
|
4355
|
-
// keeps its own fields (issue #15)
|
|
4356
|
-
//
|
|
4395
|
+
// keeps its own fields (issue #15), except a control marked
|
|
4396
|
+
// data-reactive-shared-field (issue #434): this root collects it too when
|
|
4397
|
+
// the control's own server root sits directly below this one. One level: a
|
|
4398
|
+
// grand-host never sees it. Whatever reads #collectFields sees it too: the
|
|
4399
|
+
// request, and the conditional confirm (it judges the fields the action is
|
|
4400
|
+
// about to post). Bindings, computes, listnav, dirty tracking and persist
|
|
4401
|
+
// keep the strict nearest-root #ownershipFilter / #ownsField.
|
|
4357
4402
|
#serverFieldFilter() {
|
|
4358
4403
|
const nested = this.element.querySelectorAll(AWAKE_ROOT)
|
|
4359
4404
|
if (nested.length === 0 || [...nested].every(clientOnlyRoot)) return () => true
|
|
4360
|
-
return (el) =>
|
|
4405
|
+
return (el) => {
|
|
4406
|
+
const own = serverRoot(el, AWAKE_ROOT, this.element)
|
|
4407
|
+
if (own === this.element) return true
|
|
4408
|
+
return el.getAttribute("data-reactive-shared-field") != null &&
|
|
4409
|
+
serverRoot(own.parentElement, AWAKE_ROOT, this.element) === this.element
|
|
4410
|
+
}
|
|
4361
4411
|
}
|
|
4362
4412
|
|
|
4363
4413
|
// One walk over THIS root's named controls (not a nested reactive root's),
|
|
@@ -20,6 +20,7 @@ module Phlex
|
|
|
20
20
|
include ImageFallbackBinding
|
|
21
21
|
include CountdownBinding
|
|
22
22
|
include CssVarsBinding
|
|
23
|
+
include SharedField
|
|
23
24
|
include FieldReferences
|
|
24
25
|
include ShowOptions
|
|
25
26
|
|
|
@@ -321,6 +322,16 @@ module Phlex
|
|
|
321
322
|
# span(**on(:close, shortcut: { keys: "esc", in_editable: true }))
|
|
322
323
|
# span(**on(:leave, shortcut: { keys: "esc", handled: true }))
|
|
323
324
|
#
|
|
325
|
+
# `prevent: false` (issue #433, also RESERVED) lets an ELEMENT-BOUND
|
|
326
|
+
# trigger keep the event's native default: the client skips its
|
|
327
|
+
# preventDefault, so a drop still reaches a native file input inside
|
|
328
|
+
# the trigger. Default `true` (today's behaviour, no attribute). A
|
|
329
|
+
# window-bound or outside: trigger never prevents, so there it is a
|
|
330
|
+
# no-op; with shortcut: (a chord the page claims) it raises. The
|
|
331
|
+
# forced type="button" of a click trigger stays. With confirm:, this
|
|
332
|
+
# trigger does not cancel the default; the dialog gates only its action.
|
|
333
|
+
# div(**on(:track, event: "drop", prevent: false))
|
|
334
|
+
#
|
|
324
335
|
# `optimistic:` (issue #98) — a small, ALWAYS-REVERSIBLE vocabulary of
|
|
325
336
|
# COSMETIC hints the client applies the instant the trigger fires and
|
|
326
337
|
# REVERTS if the round trip fails, so a click/toggle gives instant feedback
|
|
@@ -367,7 +378,7 @@ module Phlex
|
|
|
367
378
|
# button(**on(:destroy, confirm: "Sure?", busy: { add_class: "opacity-50" })) { "Delete" }
|
|
368
379
|
def on(action_name, event: nil, debounce: nil, throttle: nil, confirm: nil,
|
|
369
380
|
window: false, once: false, outside: false, optimistic: nil, busy: nil,
|
|
370
|
-
shortcut: nil, **params)
|
|
381
|
+
shortcut: nil, prevent: true, **params)
|
|
371
382
|
reject_removed_on_kwargs!(action_name, params)
|
|
372
383
|
# A typo'd or forgotten action renders fine and only surfaces as an
|
|
373
384
|
# opaque 403 at CLICK time (the endpoint's default-deny). Under
|
|
@@ -401,6 +412,7 @@ module Phlex
|
|
|
401
412
|
|
|
402
413
|
# Guarded here, not inside: building the caller label allocates (on() is a hot path).
|
|
403
414
|
shortcut = shortcut_for!("on(#{action_name.inspect})", shortcut, event:, window:, outside:) unless shortcut.nil?
|
|
415
|
+
keeps_default!("on(#{action_name.inspect})", prevent, shortcut) unless prevent == true
|
|
404
416
|
event ||= "click"
|
|
405
417
|
window_bound = window || outside || !shortcut.nil?
|
|
406
418
|
action = trigger_descriptors(event, shortcut, window_bound, "dispatch", once)
|
|
@@ -424,6 +436,10 @@ module Phlex
|
|
|
424
436
|
# The client decides preventDefault behavior from event.params (never by
|
|
425
437
|
# sniffing the descriptor), so EVERY window binding flags the param.
|
|
426
438
|
attrs[:data][:reactive_window_param] = "true" if window_bound
|
|
439
|
+
# Issue #433: the STRING "false" (Phlex drops a false attribute;
|
|
440
|
+
# Stimulus typecasts "false" back to false). A window binding never
|
|
441
|
+
# prevents, so it carries nothing.
|
|
442
|
+
attrs[:data][:reactive_prevent_param] = "false" unless prevent || window_bound
|
|
427
443
|
# Issue #357: the client reads the shortcut to pick the twin for its
|
|
428
444
|
# platform, skip editable targets and prevent the chord.
|
|
429
445
|
attrs[:data][:reactive_shortcut_param] = shortcut_param_attr(shortcut) if shortcut
|
|
@@ -481,6 +497,13 @@ module Phlex
|
|
|
481
497
|
# input(**on_client(:input, js.submit("#search"), debounce: 300))
|
|
482
498
|
# div(**on_client(:scroll, js.add_class("#bar", "stuck"), window: true, throttle: 250))
|
|
483
499
|
#
|
|
500
|
+
# Issue #433: `prevent: false` lets an element-bound binding keep the
|
|
501
|
+
# event's native default (on()'s rule): a drop on a native file input
|
|
502
|
+
# inside the element still sets the input's files while the ops run.
|
|
503
|
+
# It rides on the record ("prevent":false, right after "on"); a no-op
|
|
504
|
+
# on a window-bound binding, an ArgumentError with shortcut:.
|
|
505
|
+
# div(**on_client(:drop, js.remove_class("#zone", "is-over"), prevent: false))
|
|
506
|
+
#
|
|
484
507
|
# Ops are EPHEMERAL UI: any server re-render of the component (an action
|
|
485
508
|
# reply, a broadcast, a morph) rebuilds from server state and resets
|
|
486
509
|
# whatever they toggled — by design (the LiveView JS-commands caveat). Use
|
|
@@ -490,7 +513,7 @@ module Phlex
|
|
|
490
513
|
# accepted — a dead trigger should fail at render, not no-op in the
|
|
491
514
|
# browser.
|
|
492
515
|
def on_client(event, ops, window: false, once: false, outside: false, confirm: nil, detail: nil,
|
|
493
|
-
shortcut: nil, debounce: nil, throttle: nil)
|
|
516
|
+
shortcut: nil, debounce: nil, throttle: nil, prevent: true)
|
|
494
517
|
unless ops.is_a?(Phlex::Reactive::JS)
|
|
495
518
|
raise ArgumentError,
|
|
496
519
|
"on_client expects a Phlex::Reactive::JS chain (e.g. js.toggle(\"#menu\")), " \
|
|
@@ -508,8 +531,10 @@ module Phlex
|
|
|
508
531
|
"(change/input), or gate it behind a reducer's $ops / reactive_on_complete."
|
|
509
532
|
end
|
|
510
533
|
shortcut = client_shortcut!(event, shortcut, window:, outside:)
|
|
534
|
+
keeps_default!("on_client(#{event.inspect})", prevent, shortcut) unless prevent == true
|
|
511
535
|
window_bound = window || outside || !shortcut.nil?
|
|
512
|
-
|
|
536
|
+
keep_default = !prevent && !window_bound
|
|
537
|
+
record = client_binding_record(event, ops, window_bound:, outside:, once:, confirm:, detail:, keep_default:)
|
|
513
538
|
client_binding_wait!(record, event, debounce, throttle) unless debounce.nil? && throttle.nil?
|
|
514
539
|
# The record's `on` stays the bare keydown: the client reads a key
|
|
515
540
|
# filter from `on`, and `mod` is not a key.
|
|
@@ -1571,8 +1596,11 @@ module Phlex
|
|
|
1571
1596
|
# overridable confirmResolver on(:action, confirm:) uses (#52/#55); the
|
|
1572
1597
|
# client prompts BEFORE applying the ops. Issue #179: a Hash confirm: is
|
|
1573
1598
|
# CONDITIONAL, compiled by the shared compile_conditional_confirm.
|
|
1574
|
-
def client_binding_record(event, ops, window_bound:, outside:, once:, confirm:, detail:)
|
|
1599
|
+
def client_binding_record(event, ops, window_bound:, outside:, once:, confirm:, detail:, keep_default:)
|
|
1575
1600
|
record = { "on" => event }
|
|
1601
|
+
# Issue #433: RIGHT AFTER "on" — phlex/reactive/early reads the pair
|
|
1602
|
+
# `"on":"<event>","prevent":false` off the raw attribute before connect.
|
|
1603
|
+
record["prevent"] = false if keep_default
|
|
1576
1604
|
record["window"] = true if window_bound
|
|
1577
1605
|
record["outside"] = true if outside
|
|
1578
1606
|
# A mix-ed regular sibling keeps calling runOps after Stimulus drops
|
|
@@ -1889,6 +1917,17 @@ module Phlex
|
|
|
1889
1917
|
Phlex::Reactive::Shortcut.parse(shortcut)
|
|
1890
1918
|
end
|
|
1891
1919
|
|
|
1920
|
+
# Issue #433: validate prevent: once it is not the default true — only
|
|
1921
|
+
# false is left, and a shortcut (a chord the page claims) refuses it.
|
|
1922
|
+
def keeps_default!(caller, prevent, shortcut)
|
|
1923
|
+
raise ArgumentError, "#{caller} prevent: must be true or false, got #{prevent.inspect}" unless prevent == false
|
|
1924
|
+
return if shortcut.nil?
|
|
1925
|
+
|
|
1926
|
+
raise ArgumentError,
|
|
1927
|
+
"#{caller} got shortcut: with prevent: false — a shortcut is a chord the page claims, " \
|
|
1928
|
+
"so it is always prevented; drop prevent:"
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1892
1931
|
# on_client's event is positional: with shortcut: it must be the bare
|
|
1893
1932
|
# keydown (the key filter comes from the shortcut).
|
|
1894
1933
|
def client_shortcut!(event, shortcut, window:, outside:)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Reactive
|
|
5
|
+
module Component
|
|
6
|
+
# A field a nested component shares with its host (issue #434). A reactive
|
|
7
|
+
# component rendered inside another is its own root, and its named fields
|
|
8
|
+
# are its own (issue #15). A form widget with actions of its own (a picker
|
|
9
|
+
# that stores the chosen id in a hidden field) still has to post that value
|
|
10
|
+
# with the form around it:
|
|
11
|
+
#
|
|
12
|
+
# input(type: "hidden", **reactive_shared_field(name: "item[choice]", value: @choice))
|
|
13
|
+
# # => name="item[choice]" value="…" data-reactive-shared-field
|
|
14
|
+
#
|
|
15
|
+
# The marked control is collected by its own root's actions AND by the
|
|
16
|
+
# next token-bearing root above it (client-only roots in between are
|
|
17
|
+
# transparent, issue #410). One level only: a grand-host collects it only
|
|
18
|
+
# when the host marks a copy of its own. A native <form> posts it anyway,
|
|
19
|
+
# as it does any named control.
|
|
20
|
+
#
|
|
21
|
+
# The name is the HOST's param and goes on the wire verbatim, never
|
|
22
|
+
# re-scoped under this component's reactive_scope.
|
|
23
|
+
module SharedField
|
|
24
|
+
def reactive_shared_field(name:, value: nil, **attrs)
|
|
25
|
+
unless (name.is_a?(String) || name.is_a?(Symbol)) && !name.to_s.strip.empty?
|
|
26
|
+
raise ArgumentError,
|
|
27
|
+
"reactive_shared_field needs a non-blank String or Symbol name: " \
|
|
28
|
+
"(the host's param, e.g. name: \"item[choice]\"), " \
|
|
29
|
+
"got #{name.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
binding = { name: name.to_s }
|
|
33
|
+
binding[:value] = value unless value.nil?
|
|
34
|
+
binding[:data] = { reactive_shared_field: true }
|
|
35
|
+
mix(binding, attrs)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/phlex/reactive/js.rb
CHANGED
|
@@ -329,8 +329,21 @@ module Phlex
|
|
|
329
329
|
# input(type: "file", id: "photo-file", accept: "image/png,image/jpeg",
|
|
330
330
|
# **on_client(:change, js.preview_file("#photo-file", into: "#photo",
|
|
331
331
|
# wrap: "#photo-frame", max_bytes: 2_000_000, accept: true)))
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
#
|
|
333
|
+
# Several files (issue #439): `per_file: true` on an input with
|
|
334
|
+
# `multiple` checks EVERY file (accept, then max_bytes), rebuilds
|
|
335
|
+
# input.files from the accepted ones (synchronously, so an
|
|
336
|
+
# `on(:action, event: "change")` mixed in AFTER the op posts only those),
|
|
337
|
+
# dispatches one `reactive:file-rejected` per refused file
|
|
338
|
+
# (detail.reason, file, index — its place in the pick — and remaining,
|
|
339
|
+
# how many files the input keeps), and paints the first accepted image.
|
|
340
|
+
# A single input, or per_file: omitted, keeps the behaviour above.
|
|
341
|
+
# input(type: "file", id: "pick", name: "file", multiple: true, accept: "image/png",
|
|
342
|
+
# **mix(on_client(:change, js.preview_file("#pick", into: "#shot", max_bytes: 1_000_000,
|
|
343
|
+
# accept: true, per_file: true)), on(:upload, event: "change")))
|
|
344
|
+
|
|
345
|
+
def preview_file(to, into:, name: nil, global: false, valid: false, wrap: nil, max_bytes: nil, accept: nil,
|
|
346
|
+
per_file: false)
|
|
334
347
|
if [:root, ROOT_SENTINEL].include?(to)
|
|
335
348
|
raise ArgumentError,
|
|
336
349
|
"#{self.class}: preview_file reads a file input, not the component root — " \
|
|
@@ -344,6 +357,7 @@ module Phlex
|
|
|
344
357
|
args["wrap"] = preview_selector(:wrap, wrap) unless wrap.nil?
|
|
345
358
|
args["max_bytes"] = preview_max_bytes(max_bytes) unless max_bytes.nil?
|
|
346
359
|
args["accept"] = preview_accept(accept) unless accept.nil? || accept == false
|
|
360
|
+
args["per_file"] = true if preview_per_file(per_file)
|
|
347
361
|
append("preview_file", args.freeze)
|
|
348
362
|
end
|
|
349
363
|
|
|
@@ -560,6 +574,15 @@ module Phlex
|
|
|
560
574
|
"#{self.class}: preview_file max_bytes: must be a positive Integer, got #{bytes.inspect}"
|
|
561
575
|
end
|
|
562
576
|
|
|
577
|
+
# per_file: is a strict boolean (nil, like the other keywords, means
|
|
578
|
+
# omitted): a typo'd truthy value must not change which files an upload posts.
|
|
579
|
+
def preview_per_file(per_file)
|
|
580
|
+
return per_file == true if [true, false, nil].include?(per_file)
|
|
581
|
+
|
|
582
|
+
raise ArgumentError,
|
|
583
|
+
"#{self.class}: preview_file per_file: must be true, false or nil, got #{per_file.inspect}"
|
|
584
|
+
end
|
|
585
|
+
|
|
563
586
|
# true reads the input's accept attribute; a String is the list itself.
|
|
564
587
|
def preview_accept(accept)
|
|
565
588
|
return accept if accept == true || (accept.is_a?(String) && !accept.strip.empty?)
|
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.16.
|
|
4
|
+
version: 0.16.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- lib/phlex/reactive/component/presence_declaration.rb
|
|
215
215
|
- lib/phlex/reactive/component/registry.rb
|
|
216
216
|
- lib/phlex/reactive/component/selection.rb
|
|
217
|
+
- lib/phlex/reactive/component/shared_field.rb
|
|
217
218
|
- lib/phlex/reactive/component/show_options.rb
|
|
218
219
|
- lib/phlex/reactive/component/sortable.rb
|
|
219
220
|
- lib/phlex/reactive/defer.rb
|