-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Document-level horizontal swipe gestures for opening and closing the side
* panels on touch devices. Behavior follows the current layout mode:
* single-column (<= MOBILE) steps panel 1 -> 2 -> 3 on left swipes,
* two-panel (<= TABLET) steps 3 -> 1 -> 2 on right swipes, and desktop
* three-panel opens/closes 1 then 2. Swipes starting on form fields are
* ignored. While an overlay is open, panel navigation is disabled: diagram,
* table and dialog overlays swallow swipes entirely, and the code overlay
* uses them to open/close its analysis column.
*/
import { is_code_overlay_open, swipe_code_overlay_analysis } from './code-overlay.js';
/** Minimum horizontal travel, in pixels, for a swipe to register. */
const SWIPE_MIN_DISTANCE = 60;
/** Swipe counts as horizontal only when travel exceeds this multiple of vertical travel. */
const HORIZONTAL_RATIO = 1.5;
/** Elements where swipes have their own meaning or would interfere. */
const EXCLUDED_SELECTOR = 'input, textarea, select, [contenteditable]';
/** Selector matching any open modal overlay. */
const OVERLAY_SELECTOR = '.div-structure-dialog-overlay.active, .div-diagram-overlay.active, .div-code-overlay.active, .div-table-overlay.active';
let app = null;
let tracking = false;
let start_x = 0;
let start_y = 0;
/**
* Initializes the document-level swipe navigation gesture.
* @param {App} app_instance - The main application instance.
*/
export function init_swipe_navigation(app_instance) {
app = app_instance;
document.addEventListener('touchstart', on_touch_start);
document.addEventListener('touchend', on_touch_end);
document.addEventListener('touchcancel', on_touch_cancel);
}
/**
* Starts tracking a single-finger gesture unless it begins on an excluded element.
* @param {TouchEvent} e - The touch event.
*/
function on_touch_start(e) {
if (e.touches.length !== 1 || is_excluded(e.target)) {
tracking = false;
return;
}
tracking = true;
start_x = e.touches[0].clientX;
start_y = e.touches[0].clientY;
}
/**
* Evaluates the finished gesture and triggers the matching action. Swipes are
* ignored while a non-code overlay is open; while the code overlay is open,
* swipes toggle its analysis column instead of the underlying panels.
* @param {TouchEvent} e - The touch event.
*/
function on_touch_end(e) {
if (!tracking) return;
tracking = false;
if (e.touches.length !== 0) return;
const touch = e.changedTouches[0];
if (!touch) return;
const dx = touch.clientX - start_x;
const dy = touch.clientY - start_y;
if (Math.abs(dx) < SWIPE_MIN_DISTANCE || Math.abs(dx) < Math.abs(dy) * HORIZONTAL_RATIO) return;
if (document.querySelector(OVERLAY_SELECTOR)) {
if (is_code_overlay_open()) {
swipe_code_overlay_analysis(dx < 0);
}
return;
}
if (dx < 0) {
on_swipe_left();
} else {
on_swipe_right();
}
}
/**
* Cancels the tracked gesture.
*/
function on_touch_cancel() {
tracking = false;
}
/**
* Steps back one panel toward panel 1 in single-column mode (3 -> 2 -> 1,
* skipping 2 when the index is disabled). Panel 1 is the end of the chain.
* @returns {boolean} True if the view moved back a panel.
*/
export function step_panels_back_single_column() {
if (get_layout_mode() !== 'single') {
return false;
}
const panel = get_visible_panel();
if (panel === 3) {
set_panels(false, index_enabled());
return true;
}
if (panel === 2) {
set_panels(true, false);
return true;
}
return false;
}
/**
* Handles a left swipe according to the current layout mode.
*/
function on_swipe_left() {
const panel = get_visible_panel();
if (get_layout_mode() === 'single') {
// 1 -> 2 -> 3 (skipping 2 when disabled)
if (panel === 1) {
set_panels(false, index_enabled());
} else if (panel === 2) {
set_panels(false, false);
}
} else if (get_layout_mode() === 'two') {
// 2 -> 1 -> 3
if (panel === 2) {
set_panels(true, false);
} else if (panel === 1) {
set_panels(false, false);
}
} else {
// close 2 first, then 1
if (app.state_i_open) {
set_panels(true, false);
} else if (app.state_v_open) {
set_panels(false, false);
}
}
}
/**
* Handles a right swipe according to the current layout mode.
*/
function on_swipe_right() {
const panel = get_visible_panel();
if (get_layout_mode() === 'single') {
// 3 -> 2 -> 1 (skipping 2 when disabled)
step_panels_back_single_column();
} else if (get_layout_mode() === 'two') {
// 3 -> 1 -> 2
if (panel === 3) {
set_panels(true, false);
} else if (panel === 1 && index_enabled()) {
set_panels(false, true);
}
} else {
// open 1 first, then 2
if (!app.state_v_open) {
set_panels(true, false);
} else if (!app.state_i_open && index_enabled()) {
set_panels(true, true);
}
}
}
/**
* Determines the layout mode from the current window width.
* @returns {string} 'single', 'two', or 'three'.
*/
function get_layout_mode() {
if (window.innerWidth <= app.BREAKPOINT_MOBILE) return 'single';
if (window.innerWidth <= app.BREAKPOINT_TABLET) return 'two';
return 'three';
}
/**
* Determines which panel is currently visible. In single-column mode a
* conversation is shown unless exactly one side panel is open; the desktop
* three-panel branch never uses this result.
* @returns {number} 1 for the conversations list, 2 for the index, 3 for the conversation.
*/
function get_visible_panel() {
if (app.state_v_open && !app.state_i_open) return 1;
if (!app.state_v_open && app.state_i_open) return 2;
return 3;
}
/**
* Checks whether the conversation index panel is available.
* @returns {boolean} True when panel 2 can be opened.
*/
function index_enabled() {
return !!app.index_button_label;
}
/**
* Sets both panel states and refreshes the layout.
* @param {boolean} v_open - Whether panel 1 (conversations list) is open.
* @param {boolean} i_open - Whether panel 2 (index) is open.
*/
function set_panels(v_open, i_open) {
app.state_v_open = v_open;
app.state_i_open = i_open;
app.apply_panels_layout();
}
/**
* Checks whether the gesture target handles its own horizontal interaction.
* @param {EventTarget} target - The event target.
* @returns {boolean} True if the gesture should be ignored.
*/
function is_excluded(target) {
return !!(target instanceof Element && target.closest(EXCLUDED_SELECTOR));
}
swipe-navigation.js
×
Type: Web, text/x-java
6.16 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
6.16 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File