feat(runtime): support slhx drag drops
req: htmx_equivalents/002 req: dx/008 req: list/003 req: examples/001
This commit is contained in:
@@ -17,7 +17,7 @@ This is a polished Linear-style product demo for planning typed work across lane
|
|||||||
- root-scoped runtime lowering (`data-hid`, `data-sid`)
|
- root-scoped runtime lowering (`data-hid`, `data-sid`)
|
||||||
- page-enhancer navigation with native link fallback
|
- page-enhancer navigation with native link fallback
|
||||||
- SSE server push into a generated slot
|
- SSE server push into a generated slot
|
||||||
- drag-and-drop lane moves persisted by typed server handlers
|
- drag-and-drop lane moves persisted by typed server handlers through the slhx runtime
|
||||||
|
|
||||||
Verification:
|
Verification:
|
||||||
|
|
||||||
|
|||||||
@@ -367,44 +367,17 @@ fn shell(body: String) -> String {
|
|||||||
<title>slhx Techdemo</title>
|
<title>slhx Techdemo</title>
|
||||||
<script src="/slhx.js" defer></script>
|
<script src="/slhx.js" defer></script>
|
||||||
<script>
|
<script>
|
||||||
function slhxTechdemoEffect(root, data) {{
|
function slhxTechdemoLaunch(button) {{
|
||||||
|
const form = button.closest('form');
|
||||||
|
const data = new URLSearchParams(new FormData(form));
|
||||||
|
data.set('__h', button.getAttribute('data-hid'));
|
||||||
fetch('/', {{
|
fetch('/', {{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Accept': 'application/slhx' }},
|
headers: {{ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Accept': 'application/slhx' }},
|
||||||
body: data
|
body: data
|
||||||
}})
|
}})
|
||||||
.then((response) => response.arrayBuffer())
|
.then((response) => response.arrayBuffer())
|
||||||
.then((buffer) => window.slhx.applyBatch(buffer, root));
|
.then((buffer) => window.slhx.applyBatch(buffer, button.closest('[data-slhx-root]')));
|
||||||
}}
|
|
||||||
function slhxTechdemoLaunch(button) {{
|
|
||||||
const form = button.closest('form');
|
|
||||||
const data = new URLSearchParams(new FormData(form));
|
|
||||||
data.set('__h', button.getAttribute('data-hid'));
|
|
||||||
slhxTechdemoEffect(button.closest('[data-slhx-root]'), data);
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
function slhxTechdemoDrag(event) {{
|
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
|
||||||
event.dataTransfer.setData('text/plain', event.currentTarget.getAttribute('data-key'));
|
|
||||||
}}
|
|
||||||
function slhxTechdemoDragOver(event) {{
|
|
||||||
event.preventDefault();
|
|
||||||
event.currentTarget.classList.add('drop-ready');
|
|
||||||
}}
|
|
||||||
function slhxTechdemoDragLeave(event) {{
|
|
||||||
event.currentTarget.classList.remove('drop-ready');
|
|
||||||
}}
|
|
||||||
function slhxTechdemoDrop(event) {{
|
|
||||||
event.preventDefault();
|
|
||||||
const lane = event.currentTarget;
|
|
||||||
lane.classList.remove('drop-ready');
|
|
||||||
const id = event.dataTransfer.getData('text/plain');
|
|
||||||
if (!id) return false;
|
|
||||||
const data = new URLSearchParams();
|
|
||||||
data.set('__h', lane.getAttribute('data-move-hid'));
|
|
||||||
data.set('work_id', id);
|
|
||||||
data.set('lane', lane.getAttribute('data-lane'));
|
|
||||||
slhxTechdemoEffect(lane.closest('[data-slhx-root]'), data);
|
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<article +class="self.class" +data-key="self.id" draggable="true" ondragstart="slhxTechdemoDrag(event)">
|
<article +class="self.class" +data-key="self.id" draggable="true">
|
||||||
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
|
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
|
||||||
<div class="impact"><i +style="self.impact_style"></i></div>
|
<div class="impact"><i +style="self.impact_style"></i></div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<section +class="self.class" +data-lane="self.lane_id" +data-move-hid="self.move_handle" ondragover="slhxTechdemoDragOver(event)" ondragleave="slhxTechdemoDragLeave(event)" ondrop="slhxTechdemoDrop(event)">
|
<section +class="self.class" +data-lane="self.lane_id" +data-hid="self.move_handle" data-slhx-on="drop">
|
||||||
<h3>{+ self.title +}</h3>
|
<h3>{+ self.title +}</h3>
|
||||||
<p>{+ self.description +}</p>
|
<p>{+ self.description +}</p>
|
||||||
<template h-for="card in &self.cards">
|
<template h-for="card in &self.cards">
|
||||||
|
|||||||
+19
-3
@@ -10,6 +10,7 @@
|
|||||||
const timers = new WeakMap();
|
const timers = new WeakMap();
|
||||||
const sseSources = new WeakMap();
|
const sseSources = new WeakMap();
|
||||||
const atomStores = new WeakMap();
|
const atomStores = new WeakMap();
|
||||||
|
const dragKeys = new WeakMap();
|
||||||
|
|
||||||
function roots() {
|
function roots() {
|
||||||
return Array.from(document.querySelectorAll(`[${ROOT}]`));
|
return Array.from(document.querySelectorAll(`[${ROOT}]`));
|
||||||
@@ -51,11 +52,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formDataFor(el) {
|
function formDataFor(el, eventName) {
|
||||||
const form = el.tagName === "FORM" ? el : el.closest("form");
|
const form = el.tagName === "FORM" ? el : el.closest("form");
|
||||||
const data = form ? new FormData(form) : new FormData();
|
const data = form ? new FormData(form) : new FormData();
|
||||||
const id = handleId(el) || (form && handleId(form));
|
const id = handleId(el) || (form && handleId(form));
|
||||||
if (id && !data.has("__h")) data.set("__h", id);
|
if (id && !data.has("__h")) data.set("__h", id);
|
||||||
|
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
|
||||||
|
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
|
||||||
for (const { name, value } of Array.from(el.attributes || [])) {
|
for (const { name, value } of Array.from(el.attributes || [])) {
|
||||||
if (name.startsWith("data-") && !name.startsWith("data-slhx-") && name !== HID && name !== SID) {
|
if (name.startsWith("data-") && !name.startsWith("data-slhx-") && name !== HID && name !== SID) {
|
||||||
data.set(name.slice(5).replace(/-/g, "_"), value);
|
data.set(name.slice(5).replace(/-/g, "_"), value);
|
||||||
@@ -85,7 +88,7 @@
|
|||||||
|
|
||||||
async function send(el, eventName) {
|
async function send(el, eventName) {
|
||||||
if (el.getAttribute("data-slhx-confirm") && !confirm(el.getAttribute("data-slhx-confirm"))) return;
|
if (el.getAttribute("data-slhx-confirm") && !confirm(el.getAttribute("data-slhx-confirm"))) return;
|
||||||
const { form, data, multipart } = formDataFor(el);
|
const { form, data, multipart } = formDataFor(el, eventName);
|
||||||
const target = form || el;
|
const target = form || el;
|
||||||
const policy = requestPolicy(target, eventName);
|
const policy = requestPolicy(target, eventName);
|
||||||
const active = pending.get(target);
|
const active = pending.get(target);
|
||||||
@@ -386,8 +389,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindRoot(root) {
|
function bindRoot(root) {
|
||||||
["click", "submit", "input", "change"].forEach((name) => {
|
["click", "submit", "input", "change", "dragstart", "dragover", "drop"].forEach((name) => {
|
||||||
root.addEventListener(name, (event) => {
|
root.addEventListener(name, (event) => {
|
||||||
|
if (name === "dragstart") {
|
||||||
|
const item = closestInRoot(event.target, root, "[data-key]");
|
||||||
|
if (item) {
|
||||||
|
dragKeys.set(root, item.getAttribute("data-key"));
|
||||||
|
if (event.dataTransfer) event.dataTransfer.setData("text/plain", item.getAttribute("data-key"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === "dragover") {
|
||||||
|
const drop = closestInRoot(event.target, root, `[${HID}][data-slhx-on="drop"]`);
|
||||||
|
if (drop) event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const nav = closestInRoot(event.target, root, "a[data-slhx-nav], [data-slhx-boost] a[href]");
|
const nav = closestInRoot(event.target, root, "a[data-slhx-nav], [data-slhx-boost] a[href]");
|
||||||
if (name === "click" && nav && sameOriginNav(event, nav)) {
|
if (name === "click" && nav && sameOriginNav(event, nav)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ fn runtime_clicking_submitter_schedules_form_submit() {
|
|||||||
assert!(source.contains("schedule(form, \"submit\")"));
|
assert!(source.contains("schedule(form, \"submit\")"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_supports_drag_drop_params_without_user_js() {
|
||||||
|
// req: htmx_equivalents/002, req: dx/008
|
||||||
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
|
assert!(source.contains("const dragKeys = new WeakMap()"));
|
||||||
|
assert!(source.contains("dragstart"));
|
||||||
|
assert!(source.contains("dragover"));
|
||||||
|
assert!(source.contains("eventName === \"drop\""));
|
||||||
|
assert!(source.contains("data.set(\"work_id\", dragKey)"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_supports_queued_request_policy() {
|
fn runtime_supports_queued_request_policy() {
|
||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|||||||
Reference in New Issue
Block a user