recipe-planner-ui 1.5.0 → 1.6.1
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.
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/recipe-planner-ui.cjs.js +1 -1
- package/dist/cjs/rp-day-slot.cjs.entry.js +407 -17
- package/dist/cjs/rp-filter-chips_6.cjs.entry.js +132 -8
- package/dist/collection/components/rp-day-slot/rp-day-slot.css +43 -3
- package/dist/collection/components/rp-day-slot/rp-day-slot.js +408 -17
- package/dist/collection/components/rp-modal/rp-modal.css +1 -1
- package/dist/collection/components/rp-modal/rp-modal.js +43 -0
- package/dist/collection/components/rp-recipe-card/rp-recipe-card.css +13 -5
- package/dist/collection/components/rp-select/rp-select.css +5 -0
- package/dist/collection/components/rp-select/rp-select.js +86 -5
- package/dist/components/p-DE3tFqRZ.js +1 -0
- package/dist/components/rp-day-slot.js +1 -1
- package/dist/components/rp-modal.js +1 -1
- package/dist/components/rp-recipe-card.js +1 -1
- package/dist/components/rp-select.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/recipe-planner-ui.js +1 -1
- package/dist/esm/rp-day-slot.entry.js +407 -17
- package/dist/esm/rp-filter-chips_6.entry.js +132 -8
- package/dist/recipe-planner-ui/p-22441648.entry.js +1 -0
- package/dist/recipe-planner-ui/p-b3485d04.entry.js +1 -0
- package/dist/recipe-planner-ui/recipe-planner-ui.css +2 -2
- package/dist/recipe-planner-ui/recipe-planner-ui.esm.js +1 -1
- package/dist/types/components/rp-day-slot/rp-day-slot.d.ts +115 -2
- package/dist/types/components/rp-modal/rp-modal.d.ts +18 -0
- package/dist/types/components/rp-select/rp-select.d.ts +23 -0
- package/package.json +1 -1
- package/dist/components/p-q8kKM7W4.js +0 -1
- package/dist/recipe-planner-ui/p-7f595239.entry.js +0 -1
- package/dist/recipe-planner-ui/p-dcb370be.entry.js +0 -1
package/dist/cjs/loader.cjs.js
CHANGED
|
@@ -6,7 +6,7 @@ var appGlobals = require('./app-globals-V2Kpy_OQ.js');
|
|
|
6
6
|
const defineCustomElements = async (win, options) => {
|
|
7
7
|
if (typeof window === 'undefined') return undefined;
|
|
8
8
|
await appGlobals.globalScripts();
|
|
9
|
-
return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
|
|
9
|
+
return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32],"pointerKind":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
exports.setNonce = index.setNonce;
|
|
@@ -19,7 +19,7 @@ var patchBrowser = () => {
|
|
|
19
19
|
|
|
20
20
|
patchBrowser().then(async (options) => {
|
|
21
21
|
await appGlobals.globalScripts();
|
|
22
|
-
return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
|
|
22
|
+
return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32],"pointerKind":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
exports.setNonce = index.setNonce;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var index = require('./index-CrJlwqe_.js');
|
|
4
4
|
|
|
5
|
-
const rpDaySlotCss = () => `.sc-rp-day-slot-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.slot.sc-rp-day-slot{display:flex;flex-direction:column;height:100%;min-height:200px;overflow:hidden;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-xs);transition:border-color var(--rp-duration) var(--rp-ease), box-shadow var(--rp-duration) var(--rp-ease), background-color var(--rp-duration) var(--rp-ease)}.slot.is-empty.sc-rp-day-slot{background:var(--rp-color-surface-sunken);border-style:dashed}.slot.is-drop-active.sc-rp-day-slot{background:var(--rp-color-highlight-soft);border-style:solid;border-color:var(--rp-color-highlight);box-shadow:var(--rp-shadow-md), inset 0 0 0 1px var(--rp-color-highlight)}.head.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;padding:var(--rp-space-3) var(--rp-space-2) var(--rp-space-3) var(--rp-space-3);border-bottom:1px solid var(--rp-color-border)}.day.sc-rp-day-slot{min-width:0;margin:0;overflow:hidden;font-size:var(--rp-font-size-xs);font-weight:700;letter-spacing:0.08em;text-transform:uppercase;text-overflow:ellipsis;white-space:nowrap;color:var(--rp-color-text-muted)}.head-right.sc-rp-day-slot{display:flex;flex-shrink:0;gap:var(--rp-space-2);align-items:center}.count.sc-rp-day-slot{display:grid;place-items:center;min-width:20px;height:20px;padding:0 6px;font-size:0.6875rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--rp-caramel-600);background:var(--rp-color-highlight-soft);border-radius:var(--rp-radius-pill)}.add.sc-rp-day-slot{display:grid;place-items:center;width:26px;height:26px;padding:0;cursor:pointer;color:var(--rp-color-accent-contrast);background:var(--rp-color-accent);border:none;border-radius:50%;transition:transform var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.add.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.add.sc-rp-day-slot:hover{background:var(--rp-color-highlight);transform:rotate(90deg) scale(1.1)}.add.sc-rp-day-slot:active{transform:rotate(90deg) scale(0.92)}.meals.sc-rp-day-slot{flex:1;margin:0;padding:var(--rp-space-2);list-style:none;display:flex;flex-direction:column;gap:var(--rp-space-2)}.meal.sc-rp-day-slot{position:relative;padding:var(--rp-space-3) var(--rp-space-3) var(--rp-space-2);cursor:grab;touch-action:
|
|
5
|
+
const rpDaySlotCss = () => `.sc-rp-day-slot-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.slot.sc-rp-day-slot{display:flex;flex-direction:column;height:100%;min-height:200px;overflow:hidden;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-xs);transition:border-color var(--rp-duration) var(--rp-ease), box-shadow var(--rp-duration) var(--rp-ease), background-color var(--rp-duration) var(--rp-ease)}.slot.is-empty.sc-rp-day-slot{background:var(--rp-color-surface-sunken);border-style:dashed}.slot.is-drop-active.sc-rp-day-slot{background:var(--rp-color-highlight-soft);border-style:solid;border-color:var(--rp-color-highlight);box-shadow:var(--rp-shadow-md), inset 0 0 0 1px var(--rp-color-highlight)}.head.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;padding:var(--rp-space-3) var(--rp-space-2) var(--rp-space-3) var(--rp-space-3);border-bottom:1px solid var(--rp-color-border)}.day.sc-rp-day-slot{min-width:0;margin:0;overflow:hidden;font-size:var(--rp-font-size-xs);font-weight:700;letter-spacing:0.08em;text-transform:uppercase;text-overflow:ellipsis;white-space:nowrap;color:var(--rp-color-text-muted)}.head-right.sc-rp-day-slot{display:flex;flex-shrink:0;gap:var(--rp-space-2);align-items:center}.count.sc-rp-day-slot{display:grid;place-items:center;min-width:20px;height:20px;padding:0 6px;font-size:0.6875rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--rp-caramel-600);background:var(--rp-color-highlight-soft);border-radius:var(--rp-radius-pill)}.add.sc-rp-day-slot{display:grid;place-items:center;width:26px;height:26px;padding:0;cursor:pointer;color:var(--rp-color-accent-contrast);background:var(--rp-color-accent);border:none;border-radius:50%;transition:transform var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.add.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.add.sc-rp-day-slot:hover{background:var(--rp-color-highlight);transform:rotate(90deg) scale(1.1)}.add.sc-rp-day-slot:active{transform:rotate(90deg) scale(0.92)}.meals.sc-rp-day-slot{flex:1;margin:0;padding:var(--rp-space-2);list-style:none;display:flex;flex-direction:column;gap:var(--rp-space-2)}.meal.sc-rp-day-slot{position:relative;padding:var(--rp-space-3) var(--rp-space-3) var(--rp-space-2);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;background:var(--rp-color-surface-sunken);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md);transition:transform var(--rp-duration-fast) var(--rp-ease), box-shadow var(--rp-duration-fast) var(--rp-ease), opacity var(--rp-duration-fast) var(--rp-ease)}.meal.sc-rp-day-slot:hover{background:var(--rp-color-surface);box-shadow:var(--rp-shadow-sm);transform:translateY(-1px)}.meal.sc-rp-day-slot:active{cursor:grabbing}.meal.is-dragging.sc-rp-day-slot{opacity:0.4;box-shadow:none;transform:none}.grip.sc-rp-day-slot{position:absolute;inset-block-start:var(--rp-space-2);inset-inline-end:var(--rp-space-2);display:block;opacity:0;transition:opacity var(--rp-duration-fast) var(--rp-ease)}.grip.sc-rp-day-slot svg.sc-rp-day-slot{display:block;fill:var(--rp-color-text-subtle)}.meal.sc-rp-day-slot:hover .grip.sc-rp-day-slot{opacity:1}.meal-title.sc-rp-day-slot{display:block;padding-inline-end:var(--rp-space-4);font-size:var(--rp-font-size-sm);font-weight:500;line-height:1.35;color:var(--rp-color-text)}.meal-actions.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;margin-top:var(--rp-space-2)}.move-select.sc-rp-day-slot{min-width:0;position:relative;z-index:1;touch-action:manipulation}.remove.sc-rp-day-slot::before{content:'';position:absolute;inset:-9px}.meal.sc-rp-day-slot:has(rp-select[open]),.meals.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.slot.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.meal.sc-rp-day-slot:has(rp-select[open]){z-index:3}.remove.sc-rp-day-slot{display:grid;place-items:center;touch-action:manipulation;position:relative;width:24px;height:24px;padding:0;color:var(--rp-color-text-subtle);cursor:pointer;background:none;border:none;border-radius:var(--rp-radius-sm);transition:color var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.remove.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.remove.sc-rp-day-slot:hover{color:var(--rp-color-danger);background:var(--rp-color-danger-soft)}.empty.sc-rp-day-slot{display:flex;flex:1;flex-direction:column;gap:var(--rp-space-2);align-items:center;justify-content:center;padding:var(--rp-space-4) var(--rp-space-3);text-align:center}.empty-icon.sc-rp-day-slot{fill:none;stroke:var(--rp-color-text-subtle);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}.empty-text.sc-rp-day-slot{margin:0;font-size:var(--rp-font-size-xs);color:var(--rp-color-text-subtle)}.add.sc-rp-day-slot:focus-visible,.remove.sc-rp-day-slot:focus-visible,.move-select.sc-rp-day-slot:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.sr-only.sc-rp-day-slot{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@media (prefers-reduced-motion: reduce){.slot.sc-rp-day-slot,.meal.sc-rp-day-slot,.add.sc-rp-day-slot,.grip.sc-rp-day-slot,.remove.sc-rp-day-slot,.move-select.sc-rp-day-slot{transition:none}.meal.sc-rp-day-slot:hover,.add.sc-rp-day-slot:hover,.add.sc-rp-day-slot:active{transform:none}.grip.sc-rp-day-slot{opacity:1}}`;
|
|
6
6
|
|
|
7
7
|
const DaySlot = class {
|
|
8
8
|
constructor(hostRef) {
|
|
@@ -33,6 +33,19 @@ const DaySlot = class {
|
|
|
33
33
|
dropActive = false;
|
|
34
34
|
/** Id of the meal being dragged out of this column, dimmed at its origin. */
|
|
35
35
|
draggingId = null;
|
|
36
|
+
/**
|
|
37
|
+
* Which input the user is actually using, which decides whether native HTML5
|
|
38
|
+
* drag-and-drop is armed at all.
|
|
39
|
+
*
|
|
40
|
+
* It starts as `'unknown'` and is set by the first pointer event seen, rather than being
|
|
41
|
+
* guessed from `navigator.maxTouchPoints` or a media query: a laptop with a touchscreen
|
|
42
|
+
* answers yes to both while the person is using a mouse, and a tablet with a keyboard
|
|
43
|
+
* case can be either from one moment to the next. Only an actual event knows.
|
|
44
|
+
*
|
|
45
|
+
* Until something is known, native dragging stays on — that is the mouse behaviour, and
|
|
46
|
+
* a mouse cannot be detected any earlier than its first event either.
|
|
47
|
+
*/
|
|
48
|
+
pointerKind = 'unknown';
|
|
36
49
|
/**
|
|
37
50
|
* Nested children fire dragleave as the pointer crosses them, so a boolean flag would
|
|
38
51
|
* flicker off mid-column. Counting enter and leave pairs is what makes the state stable.
|
|
@@ -54,6 +67,36 @@ const DaySlot = class {
|
|
|
54
67
|
hoveredPanel = null;
|
|
55
68
|
/** Movement in CSS pixels before a press becomes a drag rather than a tap or a scroll. */
|
|
56
69
|
static DRAG_THRESHOLD = 8;
|
|
70
|
+
/**
|
|
71
|
+
* Movement tolerated during the long press before it is abandoned as a scroll.
|
|
72
|
+
*
|
|
73
|
+
* A finger resting on a card is never still: holding a phone one-handed produces several
|
|
74
|
+
* pixels of drift, and a real device reports it. With only `DRAG_THRESHOLD` to go on,
|
|
75
|
+
* that drift was read as a deliberate vertical swipe and the pending drag was discarded
|
|
76
|
+
* — the user held, and nothing happened. Every platform's long press allows a slop
|
|
77
|
+
* radius for exactly this reason; this is ours.
|
|
78
|
+
*/
|
|
79
|
+
static HOLD_SLOP = 16;
|
|
80
|
+
/**
|
|
81
|
+
* How long a finger must rest on a meal before the press becomes a drag.
|
|
82
|
+
*
|
|
83
|
+
* A phone lays the week out as a single column, so the day you are dragging to is above
|
|
84
|
+
* or below — the same axis as scrolling. Distance alone cannot separate the two
|
|
85
|
+
* intentions on that axis, so time does: hold to drag, swipe to scroll. This is the
|
|
86
|
+
* long-press convention every mobile reorder UI uses, and it is why the desktop path
|
|
87
|
+
* needs no equivalent — there the axes already differ.
|
|
88
|
+
*/
|
|
89
|
+
static LONG_PRESS_MS = 220;
|
|
90
|
+
/** Distance from a viewport edge, in CSS pixels, within which a drag scrolls the page. */
|
|
91
|
+
static EDGE_ZONE = 72;
|
|
92
|
+
/** Peak scroll speed in CSS pixels per frame, reached at the very edge. */
|
|
93
|
+
static EDGE_SPEED = 14;
|
|
94
|
+
/** Pending long-press timer, cleared if the finger moves or lifts first. */
|
|
95
|
+
holdTimer = null;
|
|
96
|
+
/** Auto-scroll frame handle, active only while a pointer drag is near an edge. */
|
|
97
|
+
edgeScrollFrame = null;
|
|
98
|
+
/** Latest pointer position, read by the auto-scroll loop between pointer events. */
|
|
99
|
+
lastPoint = { x: 0, y: 0 };
|
|
57
100
|
/** Fired when the user asks to add a meal to this day. */
|
|
58
101
|
rpAddMeal;
|
|
59
102
|
/** Fired when a meal is removed from this day. */
|
|
@@ -97,6 +140,18 @@ const DaySlot = class {
|
|
|
97
140
|
this.rpMoveMeal.emit({ id, from, to });
|
|
98
141
|
}
|
|
99
142
|
onDragStart = (event, meal) => {
|
|
143
|
+
/**
|
|
144
|
+
* Refuses a native drag that a touch produced.
|
|
145
|
+
*
|
|
146
|
+
* Belt and braces alongside clearing `draggable` on pointerdown: some Android builds
|
|
147
|
+
* evaluate the long press before that clearing is observed. A native drag started by a
|
|
148
|
+
* touch is unusable here — `dragover` and `drop` are not reliably delivered for one —
|
|
149
|
+
* and letting it run would cancel the pointer drag that *does* work.
|
|
150
|
+
*/
|
|
151
|
+
if (this.pointerKind === 'touch') {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
100
155
|
this.draggingId = meal.id;
|
|
101
156
|
// A custom MIME type is what lets dragover distinguish a meal from a dragged file or
|
|
102
157
|
// a text selection, so unrelated drags never light up a column as a drop target.
|
|
@@ -139,8 +194,9 @@ const DaySlot = class {
|
|
|
139
194
|
/**
|
|
140
195
|
* Begins tracking a possible pointer drag.
|
|
141
196
|
*
|
|
142
|
-
* Nothing happens until the
|
|
143
|
-
* the select and the remove button, and a
|
|
197
|
+
* Nothing happens until the finger has either held still for `LONG_PRESS_MS` or moved
|
|
198
|
+
* past the threshold, so a tap still reaches the select and the remove button, and a
|
|
199
|
+
* swipe still scrolls the page.
|
|
144
200
|
*/
|
|
145
201
|
onPointerDown = (event, meal) => {
|
|
146
202
|
/**
|
|
@@ -150,8 +206,22 @@ const DaySlot = class {
|
|
|
150
206
|
* so the card never dimmed and no drag image appeared even though the drop still
|
|
151
207
|
* worked. The native path is left to own the mouse entirely.
|
|
152
208
|
*/
|
|
153
|
-
if (event.pointerType === 'mouse')
|
|
209
|
+
if (event.pointerType === 'mouse') {
|
|
210
|
+
this.pointerKind = 'mouse';
|
|
154
211
|
return;
|
|
212
|
+
}
|
|
213
|
+
this.pointerKind = 'touch';
|
|
214
|
+
/**
|
|
215
|
+
* Disarms the native drag for *this* gesture, not merely the next render.
|
|
216
|
+
*
|
|
217
|
+
* Android Chrome decides whether to start an HTML5 drag during the long press that is
|
|
218
|
+
* about to happen, and a `@State` change does not reach the DOM until Stencil's next
|
|
219
|
+
* render — which lands too late to matter. Clearing the attribute directly is what
|
|
220
|
+
* stops the two drag systems arming from the same press; the state above keeps it off
|
|
221
|
+
* for every card once the render does catch up.
|
|
222
|
+
*/
|
|
223
|
+
const card = event.currentTarget;
|
|
224
|
+
card?.removeAttribute('draggable');
|
|
155
225
|
// A press on the select or a button is that control's, not the start of a drag.
|
|
156
226
|
if (event.target.closest('button, select, label, a, rp-select'))
|
|
157
227
|
return;
|
|
@@ -163,26 +233,108 @@ const DaySlot = class {
|
|
|
163
233
|
startY: event.clientY,
|
|
164
234
|
started: false,
|
|
165
235
|
};
|
|
236
|
+
this.lastPoint = { x: event.clientX, y: event.clientY };
|
|
237
|
+
// Holding still is enough to start; `onPointerMove` cancels this if the finger
|
|
238
|
+
// travels first, which is what keeps a scroll a scroll.
|
|
239
|
+
this.holdTimer = window.setTimeout(() => {
|
|
240
|
+
this.holdTimer = null;
|
|
241
|
+
if (this.pointerDrag)
|
|
242
|
+
this.beginPointerDrag();
|
|
243
|
+
}, DaySlot.LONG_PRESS_MS);
|
|
166
244
|
window.addEventListener('pointermove', this.onPointerMove, { passive: false });
|
|
167
245
|
window.addEventListener('pointerup', this.onPointerUp);
|
|
168
246
|
window.addEventListener('pointercancel', this.onPointerCancel);
|
|
169
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* Swallows a context menu raised anywhere while a touch drag is running.
|
|
250
|
+
*
|
|
251
|
+
* The element's own handler covers the press that starts the drag, but Android can also
|
|
252
|
+
* raise the menu from a descendant or once the finger has travelled onto another
|
|
253
|
+
* element, and either would hijack the touch stream mid-drag.
|
|
254
|
+
*/
|
|
255
|
+
onContextMenuDuringDrag = (event) => {
|
|
256
|
+
if (this.pointerDrag?.started)
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
};
|
|
259
|
+
/** Promotes the tracked press into an actual drag. */
|
|
260
|
+
beginPointerDrag() {
|
|
261
|
+
const drag = this.pointerDrag;
|
|
262
|
+
if (!drag || drag.started)
|
|
263
|
+
return;
|
|
264
|
+
drag.started = true;
|
|
265
|
+
this.draggingId = drag.id;
|
|
266
|
+
/**
|
|
267
|
+
* Cloned before `draggingId` is applied to the DOM.
|
|
268
|
+
*
|
|
269
|
+
* Stencil renders on its own schedule, so at this point the original card has not yet
|
|
270
|
+
* gained `.is-dragging` — which is what makes it a clean copy rather than a dimmed one.
|
|
271
|
+
* `.is-dragging` is stripped from the clone regardless, since that ordering is a
|
|
272
|
+
* detail of the framework rather than a guarantee.
|
|
273
|
+
*/
|
|
274
|
+
this.createGhost(this.cardFor(drag.id), drag.title);
|
|
275
|
+
this.moveGhost(this.lastPoint.x, this.lastPoint.y);
|
|
276
|
+
this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
|
|
277
|
+
this.startEdgeScroll();
|
|
278
|
+
document.addEventListener('contextmenu', this.onContextMenuDuringDrag, true);
|
|
279
|
+
}
|
|
170
280
|
onPointerMove = (event) => {
|
|
171
281
|
const drag = this.pointerDrag;
|
|
172
282
|
if (!drag || event.pointerId !== drag.pointerId)
|
|
173
283
|
return;
|
|
284
|
+
/**
|
|
285
|
+
* Clamped into the viewport.
|
|
286
|
+
*
|
|
287
|
+
* Pointer capture keeps delivering coordinates once a finger passes the edge of the
|
|
288
|
+
* screen, and on a phone that happens constantly — the days below the fold are reached
|
|
289
|
+
* by dragging *downwards* past the bottom. Those coordinates hit-test to nothing, so
|
|
290
|
+
* the highlight would drop out and the release would land on no column at all.
|
|
291
|
+
*
|
|
292
|
+
* Clamping keeps the effective point on the last visible row, which is also the row
|
|
293
|
+
* auto-scrolling is bringing new days into.
|
|
294
|
+
*/
|
|
295
|
+
this.lastPoint = {
|
|
296
|
+
x: Math.min(Math.max(event.clientX, 1), window.innerWidth - 1),
|
|
297
|
+
y: Math.min(Math.max(event.clientY, 1), window.innerHeight - 1),
|
|
298
|
+
};
|
|
174
299
|
if (!drag.started) {
|
|
175
|
-
const
|
|
300
|
+
const dx = Math.abs(event.clientX - drag.startX);
|
|
301
|
+
const dy = Math.abs(event.clientY - drag.startY);
|
|
302
|
+
const moved = Math.hypot(dx, dy);
|
|
303
|
+
/**
|
|
304
|
+
* Below the threshold the press is still undecided, and must stay that way.
|
|
305
|
+
*
|
|
306
|
+
* A finger resting on a card is never perfectly still — it jitters a few pixels
|
|
307
|
+
* while the long press runs. Reacting to that jitter is what broke the gesture on a
|
|
308
|
+
* real device: a wobble of a pixel or two is mostly vertical simply because of how a
|
|
309
|
+
* phone is held, so the branch below read it as a scroll and tore down the pending
|
|
310
|
+
* drag. The user held, and nothing ever happened.
|
|
311
|
+
*/
|
|
176
312
|
if (moved < DaySlot.DRAG_THRESHOLD)
|
|
177
313
|
return;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Past the threshold with the hold still pending, a mainly vertical movement is a
|
|
316
|
+
* scroll — the axis the page scrolls on. A sideways swipe cannot be a scroll here,
|
|
317
|
+
* so it starts a drag immediately, which keeps a multi-column layout responsive.
|
|
318
|
+
*
|
|
319
|
+
* The slop radius is what distinguishes a swipe from the drift of a finger that is
|
|
320
|
+
* simply resting. Inside it the hold keeps running; only travel beyond it is a
|
|
321
|
+
* decision to scroll.
|
|
322
|
+
*/
|
|
323
|
+
if (dy > dx) {
|
|
324
|
+
if (moved < DaySlot.HOLD_SLOP)
|
|
325
|
+
return;
|
|
326
|
+
this.scrollFromTouch(event);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.cancelHold();
|
|
330
|
+
this.beginPointerDrag();
|
|
181
331
|
}
|
|
182
332
|
// Suppresses the page scroll that a touch-move would otherwise produce.
|
|
183
333
|
event.preventDefault();
|
|
184
|
-
|
|
185
|
-
|
|
334
|
+
// The clamped point, not the raw one: a finger past the edge of the screen still has
|
|
335
|
+
// to highlight the row it is pressing against.
|
|
336
|
+
this.moveGhost(this.lastPoint.x, this.lastPoint.y);
|
|
337
|
+
this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
|
|
186
338
|
};
|
|
187
339
|
onPointerUp = (event) => {
|
|
188
340
|
const drag = this.pointerDrag;
|
|
@@ -190,25 +342,132 @@ const DaySlot = class {
|
|
|
190
342
|
return;
|
|
191
343
|
const started = drag.started;
|
|
192
344
|
const id = drag.id;
|
|
345
|
+
/**
|
|
346
|
+
* Resolved from the last clamped point rather than the release coordinates.
|
|
347
|
+
*
|
|
348
|
+
* A finger lifted past the bottom of the screen reports a position outside the
|
|
349
|
+
* viewport, which hit-tests to nothing — the drop would be discarded even though the
|
|
350
|
+
* column the user was holding against is obvious. `lastPoint` is that column.
|
|
351
|
+
*/
|
|
352
|
+
const panel = started ? this.panelAt(this.lastPoint.x, this.lastPoint.y) : null;
|
|
193
353
|
this.endPointerDrag();
|
|
194
354
|
if (!started)
|
|
195
355
|
return;
|
|
196
|
-
const panel = this.panelAt(event.clientX, event.clientY);
|
|
197
356
|
const to = panel?.getAttribute('day');
|
|
198
357
|
if (to)
|
|
199
358
|
this.emitMove(id, this.day, to);
|
|
200
359
|
};
|
|
201
|
-
|
|
360
|
+
/**
|
|
361
|
+
* A cancelled pointer sequence.
|
|
362
|
+
*
|
|
363
|
+
* Once a drag is under way this deliberately does *not* tear it down. On a phone
|
|
364
|
+
* `pointercancel` is delivered whenever the platform decides some other gesture owns the
|
|
365
|
+
* touch — a native drag arming, an edge-swipe, the browser's own scroll heuristics — and
|
|
366
|
+
* treating that as "the user gave up" is what made a drag vanish mid-gesture with the
|
|
367
|
+
* card left behind. The drag stays live and `pointerup` still resolves it; only a press
|
|
368
|
+
* that had not yet become a drag is discarded here.
|
|
369
|
+
*/
|
|
370
|
+
onPointerCancel = () => {
|
|
371
|
+
if (this.pointerDrag?.started)
|
|
372
|
+
return;
|
|
373
|
+
this.endPointerDrag();
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Scrolls the page for a swipe that turned out not to be a drag.
|
|
377
|
+
*
|
|
378
|
+
* The card sets `touch-action: none`, which it must in order to be draggable at all on
|
|
379
|
+
* the vertical axis — but that also means the browser will not scroll for this gesture,
|
|
380
|
+
* so the component has to. The drag is abandoned first, then each subsequent move is
|
|
381
|
+
* translated into scrolling until the finger lifts, which keeps the swipe continuous
|
|
382
|
+
* rather than dropping the rest of it.
|
|
383
|
+
*/
|
|
384
|
+
scrollFromTouch(event) {
|
|
385
|
+
const startY = this.pointerDrag?.startY ?? event.clientY;
|
|
386
|
+
let lastY = event.clientY;
|
|
387
|
+
this.endPointerDrag();
|
|
388
|
+
window.scrollBy(0, startY - event.clientY);
|
|
389
|
+
const onMove = (move) => {
|
|
390
|
+
if (move.pointerId !== event.pointerId)
|
|
391
|
+
return;
|
|
392
|
+
window.scrollBy(0, lastY - move.clientY);
|
|
393
|
+
lastY = move.clientY;
|
|
394
|
+
move.preventDefault();
|
|
395
|
+
};
|
|
396
|
+
const stop = () => {
|
|
397
|
+
window.removeEventListener('pointermove', onMove);
|
|
398
|
+
window.removeEventListener('pointerup', stop);
|
|
399
|
+
window.removeEventListener('pointercancel', stop);
|
|
400
|
+
};
|
|
401
|
+
window.addEventListener('pointermove', onMove, { passive: false });
|
|
402
|
+
window.addEventListener('pointerup', stop);
|
|
403
|
+
window.addEventListener('pointercancel', stop);
|
|
404
|
+
}
|
|
405
|
+
cancelHold() {
|
|
406
|
+
if (this.holdTimer !== null) {
|
|
407
|
+
clearTimeout(this.holdTimer);
|
|
408
|
+
this.holdTimer = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
202
411
|
endPointerDrag() {
|
|
412
|
+
this.cancelHold();
|
|
413
|
+
this.stopEdgeScroll();
|
|
414
|
+
document.removeEventListener('contextmenu', this.onContextMenuDuringDrag, true);
|
|
203
415
|
this.pointerDrag = null;
|
|
204
416
|
this.draggingId = null;
|
|
205
417
|
this.clearHighlight();
|
|
206
418
|
this.ghost?.remove();
|
|
207
419
|
this.ghost = null;
|
|
420
|
+
this.el.style.removeProperty('touch-action');
|
|
208
421
|
window.removeEventListener('pointermove', this.onPointerMove);
|
|
209
422
|
window.removeEventListener('pointerup', this.onPointerUp);
|
|
210
423
|
window.removeEventListener('pointercancel', this.onPointerCancel);
|
|
211
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Scrolls the page while the finger rests near the top or bottom of the viewport.
|
|
427
|
+
*
|
|
428
|
+
* On a phone the week is one column tall, so the day being dragged to is usually off
|
|
429
|
+
* screen — and a finger that is already touching the screen cannot scroll to it, because
|
|
430
|
+
* that same touch is the drag. Without this the drop target is simply unreachable, which
|
|
431
|
+
* is exactly the failure that a desktop browser's device emulation hides: its window is
|
|
432
|
+
* tall enough that every day is visible at once.
|
|
433
|
+
*/
|
|
434
|
+
startEdgeScroll() {
|
|
435
|
+
if (this.edgeScrollFrame !== null)
|
|
436
|
+
return;
|
|
437
|
+
const step = () => {
|
|
438
|
+
if (!this.pointerDrag?.started) {
|
|
439
|
+
this.edgeScrollFrame = null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const y = this.lastPoint.y;
|
|
443
|
+
const height = window.innerHeight;
|
|
444
|
+
let delta = 0;
|
|
445
|
+
if (y < DaySlot.EDGE_ZONE) {
|
|
446
|
+
delta = -DaySlot.EDGE_SPEED * (1 - y / DaySlot.EDGE_ZONE);
|
|
447
|
+
}
|
|
448
|
+
else if (y > height - DaySlot.EDGE_ZONE) {
|
|
449
|
+
delta = DaySlot.EDGE_SPEED * (1 - (height - y) / DaySlot.EDGE_ZONE);
|
|
450
|
+
}
|
|
451
|
+
if (delta !== 0) {
|
|
452
|
+
const before = window.scrollY;
|
|
453
|
+
window.scrollBy(0, delta);
|
|
454
|
+
// Nothing moved, so the page is already at the end — keep the highlight where it
|
|
455
|
+
// is rather than re-running a hit test that cannot produce a new answer.
|
|
456
|
+
if (window.scrollY !== before) {
|
|
457
|
+
// The page moved under a stationary finger, so what is beneath it has changed.
|
|
458
|
+
this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this.edgeScrollFrame = requestAnimationFrame(step);
|
|
462
|
+
};
|
|
463
|
+
this.edgeScrollFrame = requestAnimationFrame(step);
|
|
464
|
+
}
|
|
465
|
+
stopEdgeScroll() {
|
|
466
|
+
if (this.edgeScrollFrame !== null) {
|
|
467
|
+
cancelAnimationFrame(this.edgeScrollFrame);
|
|
468
|
+
this.edgeScrollFrame = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
212
471
|
/** The `rp-day-slot` under a point, found by hit-testing rather than by geometry. */
|
|
213
472
|
panelAt(x, y) {
|
|
214
473
|
// The ghost follows the pointer and would otherwise be the topmost element.
|
|
@@ -217,7 +476,36 @@ const DaySlot = class {
|
|
|
217
476
|
const target = document.elementFromPoint(x, y);
|
|
218
477
|
if (this.ghost)
|
|
219
478
|
this.ghost.style.display = '';
|
|
220
|
-
|
|
479
|
+
const hit = target?.closest('rp-day-slot');
|
|
480
|
+
if (hit)
|
|
481
|
+
return hit;
|
|
482
|
+
/**
|
|
483
|
+
* Hit-testing returns nothing when the point is outside the viewport or lands in the
|
|
484
|
+
* gap between columns — a finger dragged to the very edge of the screen does both.
|
|
485
|
+
* Falling back to the nearest panel by geometry means a drop at the edge still lands
|
|
486
|
+
* somewhere sensible instead of being silently discarded.
|
|
487
|
+
*/
|
|
488
|
+
return this.nearestPanel(x, y);
|
|
489
|
+
}
|
|
490
|
+
nearestPanel(x, y) {
|
|
491
|
+
let best = null;
|
|
492
|
+
let bestDistance = Infinity;
|
|
493
|
+
for (const panel of Array.from(document.querySelectorAll('rp-day-slot'))) {
|
|
494
|
+
const box = panel.getBoundingClientRect();
|
|
495
|
+
if (box.width === 0 || box.height === 0)
|
|
496
|
+
continue;
|
|
497
|
+
// Zero on either axis whenever the point is already within the panel's span there,
|
|
498
|
+
// so this measures the gap to the box rather than to its centre.
|
|
499
|
+
const dx = Math.max(box.left - x, 0, x - box.right);
|
|
500
|
+
const dy = Math.max(box.top - y, 0, y - box.bottom);
|
|
501
|
+
const distance = Math.hypot(dx, dy);
|
|
502
|
+
if (distance < bestDistance) {
|
|
503
|
+
bestDistance = distance;
|
|
504
|
+
best = panel;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Beyond this the pointer is nowhere near the planner and the drop is not meant for it.
|
|
508
|
+
return bestDistance <= 64 ? best : null;
|
|
221
509
|
}
|
|
222
510
|
highlightPanelAt(x, y) {
|
|
223
511
|
const panel = this.panelAt(x, y);
|
|
@@ -234,18 +522,89 @@ const DaySlot = class {
|
|
|
234
522
|
this.hoveredPanel?.querySelector('.slot')?.classList.remove('is-drop-active');
|
|
235
523
|
this.hoveredPanel = null;
|
|
236
524
|
}
|
|
237
|
-
|
|
525
|
+
/**
|
|
526
|
+
* The rendered card for one meal id.
|
|
527
|
+
*
|
|
528
|
+
* Found by index rather than by a selector: the `<li>` carries no id attribute, and the
|
|
529
|
+
* order of `.meal` elements is the order of `normalized`, which is what the render walks.
|
|
530
|
+
*/
|
|
531
|
+
cardFor(id) {
|
|
532
|
+
const index = this.normalized.findIndex((meal) => meal.id === id);
|
|
533
|
+
if (index < 0)
|
|
534
|
+
return null;
|
|
535
|
+
return this.el.querySelectorAll('.meal')[index] ?? null;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Builds the copy of the card that follows the finger.
|
|
539
|
+
*
|
|
540
|
+
* A clone of the real card rather than a label bearing its title. The native drag on a
|
|
541
|
+
* desktop hands the browser a snapshot of the element itself, so the thing being dragged
|
|
542
|
+
* looks exactly like the thing that was picked up; a touch drag has to construct that
|
|
543
|
+
* likeness, and a bare title made the two platforms look like different features.
|
|
544
|
+
*
|
|
545
|
+
* The clone is sized to the original because it is positioned out of the flow, where a
|
|
546
|
+
* percentage width would resolve against the viewport instead of the column.
|
|
547
|
+
*/
|
|
548
|
+
createGhost(source, title) {
|
|
238
549
|
const ghost = document.createElement('div');
|
|
239
|
-
ghost.textContent = title;
|
|
240
550
|
ghost.setAttribute('aria-hidden', 'true');
|
|
241
551
|
ghost.className = 'rp-drag-ghost';
|
|
552
|
+
if (source) {
|
|
553
|
+
const box = source.getBoundingClientRect();
|
|
554
|
+
ghost.style.width = `${box.width}px`;
|
|
555
|
+
const copy = source.cloneNode(true);
|
|
556
|
+
// The original is dimmed by `.is-dragging`; the copy is the thing being dragged and
|
|
557
|
+
// must not inherit that, nor the id-based state that belongs to the live card.
|
|
558
|
+
copy.classList.remove('is-dragging');
|
|
559
|
+
copy.removeAttribute('draggable');
|
|
560
|
+
/**
|
|
561
|
+
* Every cloned custom element is flattened to inert markup.
|
|
562
|
+
*
|
|
563
|
+
* A cloned `rp-select` is still an undefined `rp-select` in the registry's eyes, so
|
|
564
|
+
* appending it to the document upgrades it — Stencil then renders a *second* trigger
|
|
565
|
+
* inside the one already copied, and the ghost shows two dropdowns. Replacing the
|
|
566
|
+
* element with a plain `div` carrying the same classes keeps the rendered appearance
|
|
567
|
+
* while removing anything that can come alive.
|
|
568
|
+
*/
|
|
569
|
+
for (const custom of Array.from(copy.querySelectorAll('rp-select'))) {
|
|
570
|
+
const flat = document.createElement('div');
|
|
571
|
+
flat.className = custom.className;
|
|
572
|
+
// The trigger is what was visible; the list is closed and contributes nothing.
|
|
573
|
+
const trigger = custom.querySelector('.trigger');
|
|
574
|
+
if (trigger)
|
|
575
|
+
flat.append(trigger.cloneNode(true));
|
|
576
|
+
custom.replaceWith(flat);
|
|
577
|
+
}
|
|
578
|
+
for (const node of Array.from(copy.querySelectorAll('button, input, select, a'))) {
|
|
579
|
+
node.style.pointerEvents = 'none';
|
|
580
|
+
node.setAttribute('tabindex', '-1');
|
|
581
|
+
}
|
|
582
|
+
ghost.append(copy);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// No element to copy — fall back to the title, which is better than nothing at all.
|
|
586
|
+
ghost.textContent = title;
|
|
587
|
+
}
|
|
242
588
|
document.body.append(ghost);
|
|
243
589
|
this.ghost = ghost;
|
|
244
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Positions the floating copy, keeping it fully on screen.
|
|
593
|
+
*
|
|
594
|
+
* The copy is as wide as the card it came from, so following the pointer literally pushes
|
|
595
|
+
* it off the edge whenever a drag reaches one — and on a phone a drag reaches the bottom
|
|
596
|
+
* edge on almost every move, because that is how auto-scroll is triggered. Clamping to
|
|
597
|
+
* the viewport keeps the thing being dragged visible, which is the entire point of it.
|
|
598
|
+
*/
|
|
245
599
|
moveGhost(x, y) {
|
|
246
600
|
if (!this.ghost)
|
|
247
601
|
return;
|
|
248
|
-
this.ghost.
|
|
602
|
+
const box = this.ghost.getBoundingClientRect();
|
|
603
|
+
// The rule's negative margin offsets the copy up and left of the fingertip; these are
|
|
604
|
+
// the bounds that keep the result inside the viewport once it is applied.
|
|
605
|
+
const maxX = window.innerWidth - box.width + 12;
|
|
606
|
+
const maxY = window.innerHeight - box.height + 44;
|
|
607
|
+
this.ghost.style.transform = `translate(${Math.min(Math.max(x, 12), Math.max(maxX, 12))}px, ${Math.min(Math.max(y, 44), Math.max(maxY, 44))}px)`;
|
|
249
608
|
}
|
|
250
609
|
onDrop = (event) => {
|
|
251
610
|
const payload = event.dataTransfer?.getData('application/x-rp-meal');
|
|
@@ -267,7 +626,38 @@ const DaySlot = class {
|
|
|
267
626
|
const days = Array.isArray(this.days) ? this.days : [];
|
|
268
627
|
const labels = this.dayLabels ?? {};
|
|
269
628
|
const meals = this.normalized;
|
|
270
|
-
return (index.h(index.Host, { key: '
|
|
629
|
+
return (index.h(index.Host, { key: '05ee7eff0f1ff19f77363539f3f5ccf2d1d96efb' }, index.h("section", { key: 'dce7ecf1d85d7d8cd8b988fb6dc06f93810f532a', class: { slot: true, 'is-drop-active': this.dropActive, 'is-empty': meals.length === 0 }, onDragEnter: this.onDragEnter, onDragOver: this.onDragOver, onDragLeave: this.onDragLeave, onDrop: this.onDrop }, index.h("header", { key: '086dc84dce8943ae814505878fee4dbc1e3acc1a', class: "head" }, index.h("h3", { key: 'a37cb66c77bc30ed80e7eb5d9bbe38c2b2c207b6', class: "day" }, this.dayLabel || this.day), index.h("div", { key: '67e62ed1a5754842f8ffd828ac1ce7cd63a5a666', class: "head-right" }, meals.length > 0 && index.h("span", { key: 'c4460980f7998c355e00ea7d0035297b47a8ecb0', class: "count" }, meals.length), index.h("button", { key: '5d4a5c5fafa9e63cf6cd323b0e23cd6354a18d3b', type: "button", class: "add", onClick: () => this.rpAddMeal.emit(this.day), "aria-label": `Add a meal to ${(this.dayLabels ?? {})[this.day] || this.dayLabel || this.day}` }, index.h("svg", { key: '684fc27b7da23ce41a656bc04524b600dec89ee3', viewBox: "0 0 24 24", width: "15", height: "15", "aria-hidden": "true" }, index.h("path", { key: 'c22a9bdbccc3a2fee2f59106371afbfd96c9946f', d: "M12 5v14M5 12h14" }))))), meals.length === 0 ? (index.h("div", { class: "empty" }, index.h("slot", null, index.h("svg", { class: "empty-icon", viewBox: "0 0 24 24", width: "26", height: "26", "aria-hidden": "true" }, index.h("path", { d: "M4 4h16v16H4z", opacity: "0.35" }), index.h("path", { d: "M8 2v4M16 2v4M4 10h16" })), index.h("p", { class: "empty-text" }, "No meals planned")))) : (index.h("ul", { class: "meals" }, meals.map((meal) => (index.h("li", { class: { meal: true, 'is-dragging': this.draggingId === meal.id }, key: meal.id,
|
|
630
|
+
/**
|
|
631
|
+
* Only where HTML5 drag-and-drop is the right mechanism — a mouse.
|
|
632
|
+
*
|
|
633
|
+
* Android Chrome fires a native `dragstart` on a long press, so leaving
|
|
634
|
+
* this on unconditionally armed *both* systems from one touch: the
|
|
635
|
+
* pointer path began its ghost, the native drag then seized the touch
|
|
636
|
+
* stream and delivered `pointercancel`, which tore that ghost and the
|
|
637
|
+
* target highlight straight back down. The native drag it left running
|
|
638
|
+
* has no usable drop target, because a touch-initiated drag does not
|
|
639
|
+
* reliably deliver `dragover`/`drop` on Android.
|
|
640
|
+
*
|
|
641
|
+
* The visible result was a card that could be picked up, showed no
|
|
642
|
+
* highlight over any day, and refused to drop.
|
|
643
|
+
*/
|
|
644
|
+
draggable: this.pointerKind !== 'touch', onDragStart: (event) => this.onDragStart(event, meal), onPointerDown: (event) => this.onPointerDown(event, meal),
|
|
645
|
+
/**
|
|
646
|
+
* Suppresses Android's long-press context menu.
|
|
647
|
+
*
|
|
648
|
+
* The gesture that starts a drag here is the same one the platform uses
|
|
649
|
+
* to raise its text-selection and link menu. On a card containing a title
|
|
650
|
+
* and controls that menu appears over the drag, takes the touch stream
|
|
651
|
+
* with it, and leaves the next tap dismissing the menu rather than
|
|
652
|
+
* pressing the button it landed on — which is why dragging and the remove
|
|
653
|
+
* button appeared broken together.
|
|
654
|
+
*
|
|
655
|
+
* A desktop right-click is a different gesture and is left alone.
|
|
656
|
+
*/
|
|
657
|
+
onContextMenu: (event) => {
|
|
658
|
+
if (this.pointerKind === 'touch')
|
|
659
|
+
event.preventDefault();
|
|
660
|
+
} }, index.h("span", { class: "grip", "aria-hidden": "true" }, index.h("svg", { viewBox: "0 0 24 24", width: "12", height: "12" }, index.h("circle", { cx: "9", cy: "6", r: "1.4" }), index.h("circle", { cx: "15", cy: "6", r: "1.4" }), index.h("circle", { cx: "9", cy: "12", r: "1.4" }), index.h("circle", { cx: "15", cy: "12", r: "1.4" }), index.h("circle", { cx: "9", cy: "18", r: "1.4" }), index.h("circle", { cx: "15", cy: "18", r: "1.4" }))), index.h("span", { class: "meal-title" }, meal.title), index.h("div", { class: "meal-actions" }, index.h("rp-select", { class: "move-select", compact: true, label: `Move ${meal.title} to another day`, placeholder: "Move\u2026", options: days
|
|
271
661
|
.filter((day) => day !== this.day)
|
|
272
662
|
.map((day) => ({ value: day, label: labels[day] ?? day })), onRpSelectChange: (event) => this.emitMove(meal.id, this.day, event.detail) }), index.h("button", { type: "button", class: "remove", onClick: () => this.rpRemoveMeal.emit({ id: meal.id, day: this.day }), "aria-label": `Remove ${meal.title}` }, index.h("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true" }, index.h("path", { d: "M6 6l12 12M18 6L6 18" }))))))))))));
|
|
273
663
|
}
|