web: extract inline script to static/board.js
change ywyqozsoulwpwnqtwszpoksozpwswnwp
commit 597c0e18cbe9818e381f7a5442d4043ec12bd18c
author Alpha Chen <alpha@kejadlen.dev>
date
parent kuwomnku
diff --git a/src/bin/ranger/commands/serve.rs b/src/bin/ranger/commands/serve.rs
index a03bbd5..9f19616 100644
--- a/src/bin/ranger/commands/serve.rs
+++ b/src/bin/ranger/commands/serve.rs
@@ -3,7 +3,7 @@ use axum::http::StatusCode;
 use axum::http::header;
 use axum::response::{IntoResponse, Redirect};
 use axum::{Json, Router, routing::get, routing::post};
-use maud::{DOCTYPE, Markup, PreEscaped, html};
+use maud::{DOCTYPE, Markup, html};
 use ranger::key;
 use ranger::models::Task;
 use ranger::ops;
@@ -13,8 +13,9 @@ use sqlx::SqlitePool;
 use std::net::SocketAddr;
 use tokio::net::TcpListener;
 
-/// Static CSS embedded at compile time from `static/style.css`.
+/// Static assets embedded at compile time.
 const STYLE_CSS: &str = include_str!("../../../../static/style.css");
+const BOARD_JS: &str = include_str!("../../../../static/board.js");
 
 #[derive(Clone)]
 struct AppState {
@@ -36,6 +37,7 @@ pub async fn run(
         .route("/", get(index))
         .route("/b/{name}", get(board))
         .route("/static/style.css", get(serve_css))
+        .route("/static/board.js", get(serve_js))
         .route("/api/tasks/{key}/move", post(api_move_task))
         .with_state(state);
 
@@ -52,6 +54,10 @@ async fn serve_css() -> impl IntoResponse {
     ([(header::CONTENT_TYPE, "text/css")], STYLE_CSS)
 }
 
+async fn serve_js() -> impl IntoResponse {
+    ([(header::CONTENT_TYPE, "application/javascript")], BOARD_JS)
+}
+
 async fn index(State(state): State<AppState>) -> Result<Redirect, Markup> {
     // If a default backlog is set, redirect to it
     if let Some(ref name) = state.default_backlog {
@@ -306,7 +312,7 @@ async fn render_board(
                     (render_column_panel("Icebox", "state-icebox", Some("icebox"), &icebox))
                     (render_column_panel("Done", "state-done", None, &done))
                 }
-                (keyboard_nav_script())
+                script src="/static/board.js" defer {}
             }
         }
     })
@@ -437,165 +443,6 @@ fn render_task(task: &TaskView) -> Markup {
     }
 }
 
-fn keyboard_nav_script() -> Markup {
-    html! {
-        script {
-            (PreEscaped(r#"
-            (function() {
-                // === Backlog popover ===
-                document.addEventListener('click', function(e) {
-                    var dialog = document.getElementById('backlog-dialog');
-                    if (dialog && dialog.open && !dialog.contains(e.target) && !e.target.closest('.backlog-trigger')) {
-                        dialog.close();
-                    }
-                });
-
-                // === Keyboard navigation ===
-                function getFocusables() {
-                    return Array.from(document.querySelectorAll(
-                        'details.task > summary, div.task[data-key]'
-                    ));
-                }
-                function focusEl(els, idx) {
-                    if (idx >= 0 && idx < els.length) {
-                        els[idx].focus();
-                        els[idx].scrollIntoView({ block: 'nearest' });
-                    }
-                }
-                document.addEventListener('keydown', function(e) {
-                    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
-                    var els = getFocusables();
-                    var current = els.indexOf(document.activeElement);
-                    if (e.key === 'j' || e.key === 'ArrowDown') {
-                        e.preventDefault();
-                        focusEl(els, current < 0 ? 0 : current + 1);
-                    } else if (e.key === 'k' || e.key === 'ArrowUp') {
-                        e.preventDefault();
-                        focusEl(els, current < 0 ? 0 : current - 1);
-                    } else if ((e.key === 'Enter' || e.key === ' ') && document.activeElement.tagName === 'SUMMARY') {
-                        e.preventDefault();
-                        document.activeElement.click();
-                    }
-                });
-
-                // === Drag and drop (ready + icebox only) ===
-                var DRAGGABLE_STATES = ['ready', 'icebox'];
-                document.querySelectorAll('.drop-zone').forEach(function(z) {
-                    if (DRAGGABLE_STATES.indexOf(z.dataset.state) !== -1) {
-                        z.querySelectorAll('[data-key]').forEach(function(t) { t.draggable = true; });
-                    }
-                });
-                var draggedKey = null;
-                var draggedEl = null;
-
-                function getTaskEl(el) { return el.closest('[data-key]'); }
-                function getDropZone(el) { return el.closest('.drop-zone'); }
-                function isDraggableZone(zone) {
-                    return zone && DRAGGABLE_STATES.indexOf(zone.dataset.state) !== -1;
-                }
-
-                document.addEventListener('dragstart', function(e) {
-                    var task = getTaskEl(e.target);
-                    if (!task) return;
-                    var zone = getDropZone(task);
-                    if (!isDraggableZone(zone)) { e.preventDefault(); return; }
-                    draggedKey = task.dataset.key;
-                    draggedEl = task;
-                    task.classList.add('dragging');
-                    document.querySelectorAll('.drop-zone').forEach(function(z) {
-                        if (isDraggableZone(z)) z.classList.add('drag-active');
-                    });
-                    e.dataTransfer.effectAllowed = 'move';
-                    e.dataTransfer.setData('text/plain', draggedKey);
-                });
-
-                document.addEventListener('dragend', function() {
-                    if (draggedEl) draggedEl.classList.remove('dragging');
-                    document.querySelectorAll('.drop-indicator').forEach(function(el) { el.remove(); });
-                    document.querySelectorAll('.drop-zone-active').forEach(function(el) { el.classList.remove('drop-zone-active'); });
-                    document.querySelectorAll('.drag-active').forEach(function(el) { el.classList.remove('drag-active'); });
-                    draggedKey = null;
-                    draggedEl = null;
-                });
-
-                document.addEventListener('dragover', function(e) {
-                    var zone = getDropZone(e.target);
-                    if (!isDraggableZone(zone) || !draggedKey) return;
-                    e.preventDefault();
-                    e.dataTransfer.dropEffect = 'move';
-
-                    document.querySelectorAll('.drop-indicator').forEach(function(el) { el.remove(); });
-                    document.querySelectorAll('.drop-zone-active').forEach(function(el) { el.classList.remove('drop-zone-active'); });
-
-                    var tasks = Array.from(zone.querySelectorAll('[data-key]'));
-                    if (tasks.length === 0) { zone.classList.add('drop-zone-active'); return; }
-
-                    var closestTask = null, insertBefore = true, minDist = Infinity;
-                    for (var i = 0; i < tasks.length; i++) {
-                        var rect = tasks[i].getBoundingClientRect();
-                        var midY = rect.top + rect.height / 2;
-                        var dist = Math.abs(e.clientY - midY);
-                        if (dist < minDist) { minDist = dist; closestTask = tasks[i]; insertBefore = e.clientY < midY; }
-                    }
-                    if (closestTask) {
-                        var indicator = document.createElement('div');
-                        indicator.className = 'drop-indicator';
-                        closestTask.parentNode.insertBefore(indicator, insertBefore ? closestTask : closestTask.nextSibling);
-                    }
-                });
-
-                document.addEventListener('drop', function(e) {
-                    e.preventDefault();
-                    var zone = getDropZone(e.target);
-                    if (!isDraggableZone(zone) || !draggedKey) return;
-
-                    var targetState = zone.dataset.state;
-                    var tasks = Array.from(zone.querySelectorAll('[data-key]'))
-                        .filter(function(t) { return t.dataset.key !== draggedKey; });
-
-                    var beforeKey = null, afterKey = null;
-                    if (tasks.length > 0) {
-                        var closestTask = null, insertBefore = true, minDist = Infinity;
-                        for (var i = 0; i < tasks.length; i++) {
-                            var rect = tasks[i].getBoundingClientRect();
-                            var midY = rect.top + rect.height / 2;
-                            var dist = Math.abs(e.clientY - midY);
-                            if (dist < minDist) { minDist = dist; closestTask = tasks[i]; insertBefore = e.clientY < midY; }
-                        }
-                        if (closestTask) {
-                            var idx = tasks.indexOf(closestTask);
-                            if (insertBefore) {
-                                beforeKey = closestTask.dataset.key;
-                                if (idx > 0) afterKey = tasks[idx - 1].dataset.key;
-                            } else {
-                                afterKey = closestTask.dataset.key;
-                                if (idx < tasks.length - 1) beforeKey = tasks[idx + 1].dataset.key;
-                            }
-                        }
-                    }
-
-                    var body = {};
-                    var draggedZone = draggedEl ? getDropZone(draggedEl) : null;
-                    var currentState = draggedZone ? draggedZone.dataset.state : null;
-                    if (targetState !== currentState) body.state = targetState;
-                    if (beforeKey) body.before = beforeKey;
-                    if (afterKey) body.after = afterKey;
-
-                    fetch('/api/tasks/' + encodeURIComponent(draggedKey) + '/move', {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify(body)
-                    }).then(function(res) {
-                        if (res.ok) window.location.reload();
-                        else res.text().then(function(t) { console.error('Move failed:', t); });
-                    });
-                });
-            })();
-            "#))
-        }
-    }
-}
-
 async fn to_task_views(
     tasks: &[Task],
     prefixes: &std::collections::HashMap<String, usize>,
diff --git a/static/board.js b/static/board.js
new file mode 100644
index 0000000..dd28400
--- /dev/null
+++ b/static/board.js
@@ -0,0 +1,150 @@
+(function() {
+    // === Backlog popover ===
+    document.addEventListener('click', function(e) {
+        var dialog = document.getElementById('backlog-dialog');
+        if (dialog && dialog.open && !dialog.contains(e.target) && !e.target.closest('.backlog-trigger')) {
+            dialog.close();
+        }
+    });
+
+    // === Keyboard navigation ===
+    function getFocusables() {
+        return Array.from(document.querySelectorAll(
+            'details.task > summary, div.task[data-key]'
+        ));
+    }
+    function focusEl(els, idx) {
+        if (idx >= 0 && idx < els.length) {
+            els[idx].focus();
+            els[idx].scrollIntoView({ block: 'nearest' });
+        }
+    }
+    document.addEventListener('keydown', function(e) {
+        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+        var els = getFocusables();
+        var current = els.indexOf(document.activeElement);
+        if (e.key === 'j' || e.key === 'ArrowDown') {
+            e.preventDefault();
+            focusEl(els, current < 0 ? 0 : current + 1);
+        } else if (e.key === 'k' || e.key === 'ArrowUp') {
+            e.preventDefault();
+            focusEl(els, current < 0 ? 0 : current - 1);
+        } else if ((e.key === 'Enter' || e.key === ' ') && document.activeElement.tagName === 'SUMMARY') {
+            e.preventDefault();
+            document.activeElement.click();
+        }
+    });
+
+    // === Drag and drop (ready + icebox only) ===
+    var DRAGGABLE_STATES = ['ready', 'icebox'];
+    document.querySelectorAll('.drop-zone').forEach(function(z) {
+        if (DRAGGABLE_STATES.indexOf(z.dataset.state) !== -1) {
+            z.querySelectorAll('[data-key]').forEach(function(t) { t.draggable = true; });
+        }
+    });
+    var draggedKey = null;
+    var draggedEl = null;
+
+    function getTaskEl(el) { return el.closest('[data-key]'); }
+    function getDropZone(el) { return el.closest('.drop-zone'); }
+    function isDraggableZone(zone) {
+        return zone && DRAGGABLE_STATES.indexOf(zone.dataset.state) !== -1;
+    }
+
+    document.addEventListener('dragstart', function(e) {
+        var task = getTaskEl(e.target);
+        if (!task) return;
+        var zone = getDropZone(task);
+        if (!isDraggableZone(zone)) { e.preventDefault(); return; }
+        draggedKey = task.dataset.key;
+        draggedEl = task;
+        task.classList.add('dragging');
+        document.querySelectorAll('.drop-zone').forEach(function(z) {
+            if (isDraggableZone(z)) z.classList.add('drag-active');
+        });
+        e.dataTransfer.effectAllowed = 'move';
+        e.dataTransfer.setData('text/plain', draggedKey);
+    });
+
+    document.addEventListener('dragend', function() {
+        if (draggedEl) draggedEl.classList.remove('dragging');
+        document.querySelectorAll('.drop-indicator').forEach(function(el) { el.remove(); });
+        document.querySelectorAll('.drop-zone-active').forEach(function(el) { el.classList.remove('drop-zone-active'); });
+        document.querySelectorAll('.drag-active').forEach(function(el) { el.classList.remove('drag-active'); });
+        draggedKey = null;
+        draggedEl = null;
+    });
+
+    document.addEventListener('dragover', function(e) {
+        var zone = getDropZone(e.target);
+        if (!isDraggableZone(zone) || !draggedKey) return;
+        e.preventDefault();
+        e.dataTransfer.dropEffect = 'move';
+
+        document.querySelectorAll('.drop-indicator').forEach(function(el) { el.remove(); });
+        document.querySelectorAll('.drop-zone-active').forEach(function(el) { el.classList.remove('drop-zone-active'); });
+
+        var tasks = Array.from(zone.querySelectorAll('[data-key]'));
+        if (tasks.length === 0) { zone.classList.add('drop-zone-active'); return; }
+
+        var closestTask = null, insertBefore = true, minDist = Infinity;
+        for (var i = 0; i < tasks.length; i++) {
+            var rect = tasks[i].getBoundingClientRect();
+            var midY = rect.top + rect.height / 2;
+            var dist = Math.abs(e.clientY - midY);
+            if (dist < minDist) { minDist = dist; closestTask = tasks[i]; insertBefore = e.clientY < midY; }
+        }
+        if (closestTask) {
+            var indicator = document.createElement('div');
+            indicator.className = 'drop-indicator';
+            closestTask.parentNode.insertBefore(indicator, insertBefore ? closestTask : closestTask.nextSibling);
+        }
+    });
+
+    document.addEventListener('drop', function(e) {
+        e.preventDefault();
+        var zone = getDropZone(e.target);
+        if (!isDraggableZone(zone) || !draggedKey) return;
+
+        var targetState = zone.dataset.state;
+        var tasks = Array.from(zone.querySelectorAll('[data-key]'))
+            .filter(function(t) { return t.dataset.key !== draggedKey; });
+
+        var beforeKey = null, afterKey = null;
+        if (tasks.length > 0) {
+            var closestTask = null, insertBefore = true, minDist = Infinity;
+            for (var i = 0; i < tasks.length; i++) {
+                var rect = tasks[i].getBoundingClientRect();
+                var midY = rect.top + rect.height / 2;
+                var dist = Math.abs(e.clientY - midY);
+                if (dist < minDist) { minDist = dist; closestTask = tasks[i]; insertBefore = e.clientY < midY; }
+            }
+            if (closestTask) {
+                var idx = tasks.indexOf(closestTask);
+                if (insertBefore) {
+                    beforeKey = closestTask.dataset.key;
+                    if (idx > 0) afterKey = tasks[idx - 1].dataset.key;
+                } else {
+                    afterKey = closestTask.dataset.key;
+                    if (idx < tasks.length - 1) beforeKey = tasks[idx + 1].dataset.key;
+                }
+            }
+        }
+
+        var body = {};
+        var draggedZone = draggedEl ? getDropZone(draggedEl) : null;
+        var currentState = draggedZone ? draggedZone.dataset.state : null;
+        if (targetState !== currentState) body.state = targetState;
+        if (beforeKey) body.before = beforeKey;
+        if (afterKey) body.after = afterKey;
+
+        fetch('/api/tasks/' + encodeURIComponent(draggedKey) + '/move', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(body)
+        }).then(function(res) {
+            if (res.ok) window.location.reload();
+            else res.text().then(function(t) { console.error('Move failed:', t); });
+        });
+    });
+})();