vanduo-framework 1.1.8 → 1.2.0
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/README.md +42 -31
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +720 -111
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +11 -11
- package/dist/vanduo.cjs.min.js.map +3 -3
- package/dist/vanduo.css +285 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +720 -111
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +11 -11
- package/dist/vanduo.esm.min.js.map +3 -3
- package/dist/vanduo.js +720 -111
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +1 -1
- package/dist/vanduo.min.js +11 -11
- package/dist/vanduo.min.js.map +3 -3
- package/js/components/code-snippet.js +5 -3
- package/js/components/doc-search.js +90 -73
- package/js/components/grid.js +22 -22
- package/js/components/theme-customizer.js +20 -4
- package/js/components/tooltips.js +1 -1
- package/js/utils/helpers.js +24 -12
- package/js/vanduo.js +7 -7
- package/package.json +1 -1
package/dist/vanduo.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.
|
|
1
|
+
/*! Vanduo v1.2.0 | Built: 2026-02-22T21:30:31.940Z | git:64c88fd | development */
|
|
2
2
|
|
|
3
3
|
// js/utils/lifecycle.js
|
|
4
4
|
(function() {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
(function() {
|
|
109
109
|
"use strict";
|
|
110
110
|
const Vanduo2 = {
|
|
111
|
-
version: "1.
|
|
111
|
+
version: "1.2.0",
|
|
112
112
|
components: {},
|
|
113
113
|
/**
|
|
114
114
|
* Initialize framework
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
|
-
console.log("Vanduo Framework v1.
|
|
146
|
+
console.log("Vanduo Framework v1.2.0 initialized");
|
|
147
147
|
},
|
|
148
148
|
/**
|
|
149
149
|
* Register a component
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
* @param {string} name - Component name
|
|
159
159
|
*/
|
|
160
160
|
reinit: function(name) {
|
|
161
|
-
|
|
161
|
+
const component = this.components[name];
|
|
162
162
|
if (component && component.init && typeof component.init === "function") {
|
|
163
163
|
try {
|
|
164
164
|
component.init();
|
|
@@ -172,9 +172,9 @@
|
|
|
172
172
|
* Uses lifecycle manager for memory leak prevention
|
|
173
173
|
*/
|
|
174
174
|
destroyAll: function() {
|
|
175
|
-
|
|
176
|
-
for (
|
|
177
|
-
|
|
175
|
+
const names = Object.keys(this.components);
|
|
176
|
+
for (let i = 0; i < names.length; i++) {
|
|
177
|
+
const component = this.components[names[i]];
|
|
178
178
|
if (component && component.destroyAll && typeof component.destroyAll === "function") {
|
|
179
179
|
try {
|
|
180
180
|
component.destroyAll();
|
|
@@ -491,7 +491,9 @@
|
|
|
491
491
|
html = this.formatHtml(html);
|
|
492
492
|
html = this.escapeHtml(html);
|
|
493
493
|
html = this.highlightHtml(html);
|
|
494
|
-
|
|
494
|
+
const codeEl = document.createElement("code");
|
|
495
|
+
codeEl.innerHTML = html;
|
|
496
|
+
pane.replaceChildren(codeEl);
|
|
495
497
|
pane.dataset.extracted = "true";
|
|
496
498
|
},
|
|
497
499
|
/**
|
|
@@ -596,7 +598,7 @@
|
|
|
596
598
|
}
|
|
597
599
|
const codeWrapper = document.createElement("div");
|
|
598
600
|
codeWrapper.className = "vd-code-snippet-code";
|
|
599
|
-
codeWrapper.
|
|
601
|
+
codeWrapper.appendChild(code.cloneNode(true));
|
|
600
602
|
code.parentNode.removeChild(code);
|
|
601
603
|
pane.appendChild(lineNumbers);
|
|
602
604
|
pane.appendChild(codeWrapper);
|
|
@@ -1362,20 +1364,20 @@
|
|
|
1362
1364
|
// js/components/grid.js
|
|
1363
1365
|
(function() {
|
|
1364
1366
|
"use strict";
|
|
1365
|
-
|
|
1367
|
+
const supportsHas = (function() {
|
|
1366
1368
|
try {
|
|
1367
1369
|
return CSS.supports("selector(:has(*))");
|
|
1368
1370
|
} catch (_e) {
|
|
1369
1371
|
return false;
|
|
1370
1372
|
}
|
|
1371
1373
|
})();
|
|
1372
|
-
|
|
1374
|
+
const GridLayout = {
|
|
1373
1375
|
instances: /* @__PURE__ */ new Map(),
|
|
1374
1376
|
/**
|
|
1375
1377
|
* Initialize all grid layout containers
|
|
1376
1378
|
*/
|
|
1377
1379
|
init: function() {
|
|
1378
|
-
|
|
1380
|
+
const containers = document.querySelectorAll("[data-layout-mode]");
|
|
1379
1381
|
containers.forEach(function(container) {
|
|
1380
1382
|
if (this.instances.has(container)) {
|
|
1381
1383
|
return;
|
|
@@ -1389,8 +1391,8 @@
|
|
|
1389
1391
|
* @param {HTMLElement} container - Element with data-layout-mode
|
|
1390
1392
|
*/
|
|
1391
1393
|
initContainer: function(container) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
+
const mode = container.getAttribute("data-layout-mode") || "standard";
|
|
1395
|
+
const cleanupFunctions = [];
|
|
1394
1396
|
this.applyMode(container, mode);
|
|
1395
1397
|
container.setAttribute("role", "region");
|
|
1396
1398
|
container.setAttribute("aria-label", "Grid layout: " + mode + " mode");
|
|
@@ -1403,15 +1405,15 @@
|
|
|
1403
1405
|
* Initialize toggle buttons that target grid containers
|
|
1404
1406
|
*/
|
|
1405
1407
|
initToggleButtons: function() {
|
|
1406
|
-
|
|
1408
|
+
const toggleButtons = document.querySelectorAll("[data-grid-toggle]");
|
|
1407
1409
|
toggleButtons.forEach(function(button) {
|
|
1408
1410
|
if (button.getAttribute("data-grid-initialized") === "true") {
|
|
1409
1411
|
return;
|
|
1410
1412
|
}
|
|
1411
|
-
|
|
1413
|
+
const clickHandler = function(e) {
|
|
1412
1414
|
e.preventDefault();
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
+
const targetSelector = button.getAttribute("data-grid-toggle");
|
|
1416
|
+
let target;
|
|
1415
1417
|
if (targetSelector) {
|
|
1416
1418
|
target = document.querySelector(targetSelector);
|
|
1417
1419
|
} else {
|
|
@@ -1437,10 +1439,10 @@
|
|
|
1437
1439
|
*/
|
|
1438
1440
|
applyFibFallback: function(container) {
|
|
1439
1441
|
if (supportsHas) return;
|
|
1440
|
-
|
|
1442
|
+
const rows = container.querySelectorAll(".vd-row, .row");
|
|
1441
1443
|
rows.forEach(function(row) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
+
const cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
|
|
1445
|
+
const count = cols.length;
|
|
1444
1446
|
if (count === 1) {
|
|
1445
1447
|
row.style.gridTemplateColumns = "1fr";
|
|
1446
1448
|
} else if (count === 2) {
|
|
@@ -1459,7 +1461,7 @@
|
|
|
1459
1461
|
* @param {HTMLElement} container - Grid container
|
|
1460
1462
|
*/
|
|
1461
1463
|
removeFibFallback: function(container) {
|
|
1462
|
-
|
|
1464
|
+
const rows = container.querySelectorAll(".vd-row, .row");
|
|
1463
1465
|
rows.forEach(function(row) {
|
|
1464
1466
|
row.style.gridTemplateColumns = "";
|
|
1465
1467
|
});
|
|
@@ -1480,11 +1482,11 @@
|
|
|
1480
1482
|
}
|
|
1481
1483
|
container.setAttribute("data-layout-mode", mode);
|
|
1482
1484
|
container.setAttribute("aria-label", "Grid layout: " + mode + " mode");
|
|
1483
|
-
|
|
1485
|
+
const toggleButtons = document.querySelectorAll("[data-grid-toggle]");
|
|
1484
1486
|
toggleButtons.forEach(function(btn) {
|
|
1485
|
-
|
|
1487
|
+
const targetSelector = btn.getAttribute("data-grid-toggle");
|
|
1486
1488
|
if (targetSelector && container.matches(targetSelector)) {
|
|
1487
|
-
|
|
1489
|
+
const isActive = mode === "fibonacci";
|
|
1488
1490
|
if (isActive) {
|
|
1489
1491
|
btn.classList.add("is-active");
|
|
1490
1492
|
} else {
|
|
@@ -1493,11 +1495,11 @@
|
|
|
1493
1495
|
btn.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
1494
1496
|
}
|
|
1495
1497
|
});
|
|
1496
|
-
|
|
1498
|
+
const instance = this.instances.get(container);
|
|
1497
1499
|
if (instance) {
|
|
1498
1500
|
instance.mode = mode;
|
|
1499
1501
|
}
|
|
1500
|
-
|
|
1502
|
+
let event;
|
|
1501
1503
|
try {
|
|
1502
1504
|
event = new CustomEvent("grid:modechange", {
|
|
1503
1505
|
bubbles: true,
|
|
@@ -1524,8 +1526,8 @@
|
|
|
1524
1526
|
container = document.querySelector(container);
|
|
1525
1527
|
}
|
|
1526
1528
|
if (!container) return;
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
+
const currentMode = container.getAttribute("data-layout-mode") || "standard";
|
|
1530
|
+
const newMode = currentMode === "fibonacci" ? "standard" : "fibonacci";
|
|
1529
1531
|
this.applyMode(container, newMode);
|
|
1530
1532
|
},
|
|
1531
1533
|
/**
|
|
@@ -1558,7 +1560,7 @@
|
|
|
1558
1560
|
* @param {HTMLElement} container - Grid container
|
|
1559
1561
|
*/
|
|
1560
1562
|
destroy: function(container) {
|
|
1561
|
-
|
|
1563
|
+
const instance = this.instances.get(container);
|
|
1562
1564
|
if (!instance) return;
|
|
1563
1565
|
instance.cleanup.forEach(function(fn) {
|
|
1564
1566
|
fn();
|
|
@@ -1575,7 +1577,7 @@
|
|
|
1575
1577
|
this.instances.forEach(function(instance, container) {
|
|
1576
1578
|
this.destroy(container);
|
|
1577
1579
|
}.bind(this));
|
|
1578
|
-
|
|
1580
|
+
const toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
|
|
1579
1581
|
toggleButtons.forEach(function(button) {
|
|
1580
1582
|
if (button._gridCleanup) {
|
|
1581
1583
|
button._gridCleanup();
|
|
@@ -3672,9 +3674,9 @@
|
|
|
3672
3674
|
},
|
|
3673
3675
|
// Default values
|
|
3674
3676
|
DEFAULTS: {
|
|
3675
|
-
PRIMARY_LIGHT: "
|
|
3677
|
+
PRIMARY_LIGHT: "black",
|
|
3676
3678
|
PRIMARY_DARK: "amber",
|
|
3677
|
-
NEUTRAL: "
|
|
3679
|
+
NEUTRAL: "neutral",
|
|
3678
3680
|
RADIUS: "0.5",
|
|
3679
3681
|
FONT: "ubuntu",
|
|
3680
3682
|
THEME: "system"
|
|
@@ -4037,21 +4039,33 @@
|
|
|
4037
4039
|
* Generate panel HTML
|
|
4038
4040
|
*/
|
|
4039
4041
|
getPanelHTML: function() {
|
|
4042
|
+
const esc = typeof escapeHtml === "function" ? escapeHtml : function(text) {
|
|
4043
|
+
const div = document.createElement("div");
|
|
4044
|
+
div.textContent = String(text ?? "");
|
|
4045
|
+
return div.innerHTML;
|
|
4046
|
+
};
|
|
4047
|
+
const safeColor = function(value) {
|
|
4048
|
+
const normalized = String(value ?? "").trim();
|
|
4049
|
+
if (/^(#[0-9a-fA-F]{3,8}|rgb[a]?\([^)]{1,60}\)|hsl[a]?\([^)]{1,60}\)|var\(--[a-zA-Z0-9_-]{1,40}\))$/.test(normalized)) {
|
|
4050
|
+
return normalized;
|
|
4051
|
+
}
|
|
4052
|
+
return "#000000";
|
|
4053
|
+
};
|
|
4040
4054
|
let primarySwatches = "";
|
|
4041
4055
|
for (const [key, value] of Object.entries(this.PRIMARY_COLORS)) {
|
|
4042
|
-
primarySwatches += `<button class="tc-color-swatch${key === this.state.primary ? " is-active" : ""}" data-color="${key}" style="--swatch-color: ${value.color}" title="${value.name}"></button>`;
|
|
4056
|
+
primarySwatches += `<button class="tc-color-swatch${key === this.state.primary ? " is-active" : ""}" data-color="${esc(key)}" style="--swatch-color: ${safeColor(value.color)}" title="${esc(value.name)}"></button>`;
|
|
4043
4057
|
}
|
|
4044
4058
|
let neutralSwatches = "";
|
|
4045
4059
|
for (const [key, value] of Object.entries(this.NEUTRAL_COLORS)) {
|
|
4046
|
-
neutralSwatches += `<button class="tc-neutral-swatch${key === this.state.neutral ? " is-active" : ""}" data-neutral="${key}" style="--swatch-color: ${value.color}" title="${value.name}"><span>${value.name}</span></button>`;
|
|
4060
|
+
neutralSwatches += `<button class="tc-neutral-swatch${key === this.state.neutral ? " is-active" : ""}" data-neutral="${esc(key)}" style="--swatch-color: ${safeColor(value.color)}" title="${esc(value.name)}"><span>${esc(value.name)}</span></button>`;
|
|
4047
4061
|
}
|
|
4048
4062
|
let radiusButtons = "";
|
|
4049
4063
|
this.RADIUS_OPTIONS.forEach((r) => {
|
|
4050
|
-
radiusButtons += `<button class="tc-radius-btn${r === this.state.radius ? " is-active" : ""}" data-radius="${r}">${r}</button>`;
|
|
4064
|
+
radiusButtons += `<button class="tc-radius-btn${r === this.state.radius ? " is-active" : ""}" data-radius="${esc(r)}">${esc(r)}</button>`;
|
|
4051
4065
|
});
|
|
4052
4066
|
let fontOptions = "";
|
|
4053
4067
|
for (const [key, value] of Object.entries(this.FONT_OPTIONS)) {
|
|
4054
|
-
fontOptions += `<option value="${key}"${key === this.state.font ? " selected" : ""}>${value.name}</option>`;
|
|
4068
|
+
fontOptions += `<option value="${esc(key)}"${key === this.state.font ? " selected" : ""}>${esc(value.name)}</option>`;
|
|
4055
4069
|
}
|
|
4056
4070
|
const modeIcons = {
|
|
4057
4071
|
"system": "ph-desktop",
|
|
@@ -4109,6 +4123,13 @@
|
|
|
4109
4123
|
/**
|
|
4110
4124
|
* Bind event listeners
|
|
4111
4125
|
*/
|
|
4126
|
+
/**
|
|
4127
|
+
* Check whether the current primary color is one of the auto-defaults
|
|
4128
|
+
* (i.e. the user hasn't explicitly picked a non-default color).
|
|
4129
|
+
*/
|
|
4130
|
+
isUsingDefaultPrimary: function() {
|
|
4131
|
+
return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT || this.state.primary === this.DEFAULTS.PRIMARY_DARK;
|
|
4132
|
+
},
|
|
4112
4133
|
bindEvents: function() {
|
|
4113
4134
|
if (this.elements.trigger) {
|
|
4114
4135
|
this.addListener(this.elements.trigger, "click", (e) => {
|
|
@@ -4118,6 +4139,20 @@
|
|
|
4118
4139
|
});
|
|
4119
4140
|
}
|
|
4120
4141
|
this.bindPanelEvents();
|
|
4142
|
+
if (window.matchMedia) {
|
|
4143
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
4144
|
+
const handler = () => {
|
|
4145
|
+
if (this.state.theme === "system" && this.isUsingDefaultPrimary()) {
|
|
4146
|
+
const newDefault = this.getDefaultPrimary("system");
|
|
4147
|
+
if (newDefault !== this.state.primary) {
|
|
4148
|
+
this.applyPrimary(newDefault);
|
|
4149
|
+
this.updateUI();
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
mq.addEventListener("change", handler);
|
|
4154
|
+
this._cleanup.push(() => mq.removeEventListener("change", handler));
|
|
4155
|
+
}
|
|
4121
4156
|
this.addListener(document, "click", (e) => {
|
|
4122
4157
|
if (this.state.isOpen && this.elements.customizer && !this.elements.customizer.contains(e.target)) {
|
|
4123
4158
|
this.close();
|
|
@@ -4679,7 +4714,7 @@
|
|
|
4679
4714
|
if (typeof sanitizeHtml === "function") {
|
|
4680
4715
|
return sanitizeHtml(input);
|
|
4681
4716
|
}
|
|
4682
|
-
|
|
4717
|
+
const div = document.createElement("div");
|
|
4683
4718
|
div.textContent = input || "";
|
|
4684
4719
|
return div.innerHTML;
|
|
4685
4720
|
},
|
|
@@ -4918,7 +4953,7 @@
|
|
|
4918
4953
|
// js/components/doc-search.js
|
|
4919
4954
|
(function() {
|
|
4920
4955
|
"use strict";
|
|
4921
|
-
|
|
4956
|
+
const DEFAULTS = {
|
|
4922
4957
|
// Behavior
|
|
4923
4958
|
minQueryLength: 2,
|
|
4924
4959
|
maxResults: 10,
|
|
@@ -4965,8 +5000,8 @@
|
|
|
4965
5000
|
placeholder: "Search..."
|
|
4966
5001
|
};
|
|
4967
5002
|
function createSearch(options) {
|
|
4968
|
-
|
|
4969
|
-
|
|
5003
|
+
const config = Object.assign({}, DEFAULTS, options || {});
|
|
5004
|
+
const state = {
|
|
4970
5005
|
initialized: false,
|
|
4971
5006
|
index: [],
|
|
4972
5007
|
results: [],
|
|
@@ -4979,6 +5014,21 @@
|
|
|
4979
5014
|
debounceTimer: null,
|
|
4980
5015
|
boundHandlers: {}
|
|
4981
5016
|
};
|
|
5017
|
+
function safeInvokeCallback(name, fn, ...args) {
|
|
5018
|
+
try {
|
|
5019
|
+
fn(...args);
|
|
5020
|
+
} catch (error) {
|
|
5021
|
+
console.warn('[Vanduo Search] Callback error in "' + name + '":', error);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function setResultsHtml(html) {
|
|
5025
|
+
if (!state.resultsContainer) return;
|
|
5026
|
+
try {
|
|
5027
|
+
state.resultsContainer.innerHTML = html;
|
|
5028
|
+
} catch (error) {
|
|
5029
|
+
console.warn("[Vanduo Search] Failed to render results:", error);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
4982
5032
|
function init() {
|
|
4983
5033
|
if (state.initialized) {
|
|
4984
5034
|
return instance;
|
|
@@ -5020,20 +5070,20 @@
|
|
|
5020
5070
|
});
|
|
5021
5071
|
return;
|
|
5022
5072
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5073
|
+
const sections = document.querySelectorAll(config.contentSelector);
|
|
5074
|
+
const categoryMap = buildCategoryMap();
|
|
5025
5075
|
sections.forEach(function(section) {
|
|
5026
|
-
|
|
5076
|
+
const id = section.id;
|
|
5027
5077
|
if (!id) return;
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5078
|
+
const titleEl = section.querySelector(config.titleSelector);
|
|
5079
|
+
const title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, "").trim() : id;
|
|
5080
|
+
const category = categoryMap[id] || "Documentation";
|
|
5081
|
+
const content = extractContent(section);
|
|
5082
|
+
const keywords = extractKeywords(section, title);
|
|
5083
|
+
const iconEl = titleEl ? titleEl.querySelector("i.ph") : null;
|
|
5084
|
+
let icon = "";
|
|
5035
5085
|
if (iconEl && iconEl.classList) {
|
|
5036
|
-
for (
|
|
5086
|
+
for (let ci = 0; ci < iconEl.classList.length; ci++) {
|
|
5037
5087
|
if (iconEl.classList[ci].indexOf("ph-") === 0) {
|
|
5038
5088
|
icon = iconEl.classList[ci];
|
|
5039
5089
|
break;
|
|
@@ -5053,16 +5103,16 @@
|
|
|
5053
5103
|
});
|
|
5054
5104
|
}
|
|
5055
5105
|
function buildCategoryMap() {
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5106
|
+
const map = {};
|
|
5107
|
+
let currentCategory = "Documentation";
|
|
5108
|
+
const navItems = document.querySelectorAll(config.navSelector + ", " + config.sectionSelector);
|
|
5059
5109
|
navItems.forEach(function(item) {
|
|
5060
5110
|
if (item.classList.contains("doc-nav-section")) {
|
|
5061
5111
|
currentCategory = item.textContent.trim();
|
|
5062
5112
|
} else {
|
|
5063
|
-
|
|
5113
|
+
const href = item.getAttribute("href");
|
|
5064
5114
|
if (href && href.startsWith("#")) {
|
|
5065
|
-
|
|
5115
|
+
const id = href.substring(1);
|
|
5066
5116
|
map[id] = currentCategory;
|
|
5067
5117
|
}
|
|
5068
5118
|
}
|
|
@@ -5070,35 +5120,35 @@
|
|
|
5070
5120
|
return map;
|
|
5071
5121
|
}
|
|
5072
5122
|
function extractContent(section) {
|
|
5073
|
-
|
|
5074
|
-
|
|
5123
|
+
const clone = section.cloneNode(true);
|
|
5124
|
+
const toRemove = clone.querySelectorAll(config.excludeFromContent);
|
|
5075
5125
|
toRemove.forEach(function(el) {
|
|
5076
5126
|
el.remove();
|
|
5077
5127
|
});
|
|
5078
|
-
|
|
5128
|
+
let text = clone.textContent || "";
|
|
5079
5129
|
text = text.replace(/\s+/g, " ").trim();
|
|
5080
5130
|
return text.substring(0, config.maxContentLength);
|
|
5081
5131
|
}
|
|
5082
5132
|
function extractKeywords(section, title) {
|
|
5083
|
-
|
|
5133
|
+
const keywords = [];
|
|
5084
5134
|
title.toLowerCase().split(/\s+/).forEach(function(word) {
|
|
5085
5135
|
if (word.length > 2) {
|
|
5086
5136
|
keywords.push(word);
|
|
5087
5137
|
}
|
|
5088
5138
|
});
|
|
5089
|
-
|
|
5139
|
+
const codeBlocks = section.querySelectorAll("code");
|
|
5090
5140
|
codeBlocks.forEach(function(code) {
|
|
5091
|
-
|
|
5092
|
-
|
|
5141
|
+
const text = code.textContent || "";
|
|
5142
|
+
const classMatches = text.match(/\.([\w-]+)/g);
|
|
5093
5143
|
if (classMatches) {
|
|
5094
5144
|
classMatches.forEach(function(match) {
|
|
5095
5145
|
keywords.push(match.substring(1).toLowerCase());
|
|
5096
5146
|
});
|
|
5097
5147
|
}
|
|
5098
5148
|
});
|
|
5099
|
-
|
|
5149
|
+
const dataAttrs = section.querySelectorAll("[data-tooltip], [data-modal]");
|
|
5100
5150
|
dataAttrs.forEach(function(el) {
|
|
5101
|
-
|
|
5151
|
+
const attrs = el.getAttributeNames().filter(function(name) {
|
|
5102
5152
|
return name.startsWith("data-");
|
|
5103
5153
|
});
|
|
5104
5154
|
attrs.forEach(function(attr) {
|
|
@@ -5108,7 +5158,7 @@
|
|
|
5108
5158
|
return Array.from(new Set(keywords));
|
|
5109
5159
|
}
|
|
5110
5160
|
function extractKeywordsFromText(text) {
|
|
5111
|
-
|
|
5161
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
5112
5162
|
return words.filter(function(word) {
|
|
5113
5163
|
return word.length > 2;
|
|
5114
5164
|
});
|
|
@@ -5141,9 +5191,9 @@
|
|
|
5141
5191
|
}
|
|
5142
5192
|
};
|
|
5143
5193
|
state.boundHandlers.handleResultClick = function(e) {
|
|
5144
|
-
|
|
5194
|
+
const result = e.target.closest(".vd-doc-search-result");
|
|
5145
5195
|
if (result) {
|
|
5146
|
-
|
|
5196
|
+
const index = parseInt(result.dataset.index, 10);
|
|
5147
5197
|
select(index);
|
|
5148
5198
|
}
|
|
5149
5199
|
};
|
|
@@ -5167,7 +5217,7 @@
|
|
|
5167
5217
|
}
|
|
5168
5218
|
}
|
|
5169
5219
|
function setupAria() {
|
|
5170
|
-
|
|
5220
|
+
const resultsId = state.resultsContainer.id || "search-results-" + Math.random().toString(36).substr(2, 9);
|
|
5171
5221
|
state.resultsContainer.id = resultsId;
|
|
5172
5222
|
state.input.setAttribute("role", "combobox");
|
|
5173
5223
|
state.input.setAttribute("aria-autocomplete", "list");
|
|
@@ -5177,7 +5227,7 @@
|
|
|
5177
5227
|
state.resultsContainer.setAttribute("aria-label", "Search results");
|
|
5178
5228
|
}
|
|
5179
5229
|
function handleInput(e) {
|
|
5180
|
-
|
|
5230
|
+
const query = e.target.value.trim();
|
|
5181
5231
|
if (state.debounceTimer) {
|
|
5182
5232
|
clearTimeout(state.debounceTimer);
|
|
5183
5233
|
}
|
|
@@ -5192,7 +5242,7 @@
|
|
|
5192
5242
|
render();
|
|
5193
5243
|
open();
|
|
5194
5244
|
if (typeof config.onSearch === "function") {
|
|
5195
|
-
config.onSearch
|
|
5245
|
+
safeInvokeCallback("onSearch", config.onSearch, query, state.results);
|
|
5196
5246
|
}
|
|
5197
5247
|
}, config.debounceMs);
|
|
5198
5248
|
}
|
|
@@ -5233,15 +5283,15 @@
|
|
|
5233
5283
|
}
|
|
5234
5284
|
}
|
|
5235
5285
|
function search(query) {
|
|
5236
|
-
|
|
5286
|
+
const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
|
|
5237
5287
|
return t.length > 0;
|
|
5238
5288
|
});
|
|
5239
|
-
|
|
5289
|
+
const scored = [];
|
|
5240
5290
|
state.index.forEach(function(entry) {
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5291
|
+
let score = 0;
|
|
5292
|
+
const titleLower = entry.title.toLowerCase();
|
|
5293
|
+
const categoryLower = entry.category.toLowerCase();
|
|
5294
|
+
const contentLower = entry.content.toLowerCase();
|
|
5245
5295
|
terms.forEach(function(term) {
|
|
5246
5296
|
if (titleLower.includes(term)) {
|
|
5247
5297
|
score += 100;
|
|
@@ -5254,7 +5304,7 @@
|
|
|
5254
5304
|
if (categoryLower.includes(term)) {
|
|
5255
5305
|
score += 50;
|
|
5256
5306
|
}
|
|
5257
|
-
|
|
5307
|
+
const keywordMatch = entry.keywords.some(function(k) {
|
|
5258
5308
|
return k.includes(term);
|
|
5259
5309
|
});
|
|
5260
5310
|
if (keywordMatch) {
|
|
@@ -5284,19 +5334,19 @@
|
|
|
5284
5334
|
}
|
|
5285
5335
|
function render() {
|
|
5286
5336
|
if (state.results.length === 0) {
|
|
5287
|
-
|
|
5337
|
+
setResultsHtml(renderEmpty());
|
|
5288
5338
|
return;
|
|
5289
5339
|
}
|
|
5290
|
-
|
|
5340
|
+
let html = '<ul class="vd-doc-search-results-list" role="listbox">';
|
|
5291
5341
|
state.results.forEach(function(result, index) {
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5342
|
+
const isActive = index === state.activeIndex;
|
|
5343
|
+
const icon = result.icon || getCategoryIcon(result.categorySlug);
|
|
5344
|
+
const excerpt = getExcerpt(result.content, state.query);
|
|
5295
5345
|
html += '<li class="vd-doc-search-result' + (isActive ? " is-active" : "") + '" role="option" id="vd-doc-search-result-' + index + '" data-index="' + index + '" data-category="' + escapeHtml2(result.categorySlug) + '" aria-selected="' + isActive + '"><div class="vd-doc-search-result-icon"><i class="ph ' + escapeHtml2(icon) + '"></i></div><div class="vd-doc-search-result-content"><div class="vd-doc-search-result-title">' + highlight(result.title, state.query) + '</div><div class="vd-doc-search-result-category">' + escapeHtml2(result.category) + '</div><div class="vd-doc-search-result-excerpt">' + highlight(excerpt, state.query) + "</div></div></li>";
|
|
5296
5346
|
});
|
|
5297
5347
|
html += "</ul>";
|
|
5298
5348
|
html += renderFooter();
|
|
5299
|
-
|
|
5349
|
+
setResultsHtml(html);
|
|
5300
5350
|
}
|
|
5301
5351
|
function renderEmpty() {
|
|
5302
5352
|
return '<div class="vd-doc-search-empty"><div class="vd-doc-search-empty-icon"><i class="ph ph-magnifying-glass"></i></div><div class="vd-doc-search-empty-title">' + escapeHtml2(config.emptyTitle) + '</div><div class="vd-doc-search-empty-text">' + escapeHtml2(config.emptyText) + "</div></div>";
|
|
@@ -5308,12 +5358,12 @@
|
|
|
5308
5358
|
return config.categoryIcons[categorySlug] || config.categoryIcons["default"] || "ph-file-text";
|
|
5309
5359
|
}
|
|
5310
5360
|
function getExcerpt(content, query) {
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
for (
|
|
5316
|
-
|
|
5361
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
5362
|
+
const contentLower = content.toLowerCase();
|
|
5363
|
+
const excerptLength = 100;
|
|
5364
|
+
let matchPos = -1;
|
|
5365
|
+
for (let i = 0; i < terms.length; i++) {
|
|
5366
|
+
const pos = contentLower.indexOf(terms[i]);
|
|
5317
5367
|
if (pos !== -1 && (matchPos === -1 || pos < matchPos)) {
|
|
5318
5368
|
matchPos = pos;
|
|
5319
5369
|
}
|
|
@@ -5321,9 +5371,9 @@
|
|
|
5321
5371
|
if (matchPos === -1) {
|
|
5322
5372
|
return content.substring(0, excerptLength) + "...";
|
|
5323
5373
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5374
|
+
const start = Math.max(0, matchPos - 30);
|
|
5375
|
+
const end = Math.min(content.length, matchPos + excerptLength);
|
|
5376
|
+
let excerpt = content.substring(start, end);
|
|
5327
5377
|
if (start > 0) {
|
|
5328
5378
|
excerpt = "..." + excerpt;
|
|
5329
5379
|
}
|
|
@@ -5334,24 +5384,24 @@
|
|
|
5334
5384
|
}
|
|
5335
5385
|
function highlight(text, query) {
|
|
5336
5386
|
if (!query) return escapeHtml2(text);
|
|
5337
|
-
|
|
5387
|
+
const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
|
|
5338
5388
|
return t.length > 0;
|
|
5339
5389
|
});
|
|
5340
|
-
|
|
5390
|
+
let escaped = escapeHtml2(text);
|
|
5341
5391
|
terms.forEach(function(term) {
|
|
5342
5392
|
if (term.length > 50) return;
|
|
5343
|
-
|
|
5393
|
+
const regex = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
|
|
5344
5394
|
escaped = escaped.replace(regex, "<" + config.highlightTag + ">$1</" + config.highlightTag + ">");
|
|
5345
5395
|
});
|
|
5346
5396
|
return escaped;
|
|
5347
5397
|
}
|
|
5348
5398
|
function escapeHtml2(text) {
|
|
5349
|
-
|
|
5399
|
+
const div = document.createElement("div");
|
|
5350
5400
|
div.textContent = text;
|
|
5351
5401
|
return div.innerHTML;
|
|
5352
5402
|
}
|
|
5353
5403
|
function navigate(direction) {
|
|
5354
|
-
|
|
5404
|
+
let newIndex = state.activeIndex + direction;
|
|
5355
5405
|
if (newIndex < 0) {
|
|
5356
5406
|
newIndex = state.results.length - 1;
|
|
5357
5407
|
} else if (newIndex >= state.results.length) {
|
|
@@ -5360,13 +5410,13 @@
|
|
|
5360
5410
|
setActiveIndex(newIndex);
|
|
5361
5411
|
}
|
|
5362
5412
|
function setActiveIndex(index) {
|
|
5363
|
-
|
|
5413
|
+
const prevActive = state.resultsContainer.querySelector(".vd-doc-search-result.is-active");
|
|
5364
5414
|
if (prevActive) {
|
|
5365
5415
|
prevActive.classList.remove("is-active");
|
|
5366
5416
|
prevActive.setAttribute("aria-selected", "false");
|
|
5367
5417
|
}
|
|
5368
5418
|
state.activeIndex = index;
|
|
5369
|
-
|
|
5419
|
+
const newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
|
|
5370
5420
|
if (newActive) {
|
|
5371
5421
|
newActive.classList.add("is-active");
|
|
5372
5422
|
newActive.setAttribute("aria-selected", "true");
|
|
@@ -5375,16 +5425,16 @@
|
|
|
5375
5425
|
}
|
|
5376
5426
|
}
|
|
5377
5427
|
function select(index) {
|
|
5378
|
-
|
|
5428
|
+
const result = state.results[index];
|
|
5379
5429
|
if (!result) return;
|
|
5380
5430
|
close();
|
|
5381
5431
|
state.input.value = "";
|
|
5382
5432
|
state.query = "";
|
|
5383
5433
|
if (typeof config.onSelect === "function") {
|
|
5384
|
-
config.onSelect
|
|
5434
|
+
safeInvokeCallback("onSelect", config.onSelect, result);
|
|
5385
5435
|
return;
|
|
5386
5436
|
}
|
|
5387
|
-
|
|
5437
|
+
const section = document.querySelector(result.url);
|
|
5388
5438
|
if (section) {
|
|
5389
5439
|
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
5390
5440
|
window.history.pushState(null, "", result.url);
|
|
@@ -5392,7 +5442,7 @@
|
|
|
5392
5442
|
}
|
|
5393
5443
|
}
|
|
5394
5444
|
function updateSidebarActive(sectionId) {
|
|
5395
|
-
|
|
5445
|
+
const navLinks = document.querySelectorAll(config.navSelector);
|
|
5396
5446
|
navLinks.forEach(function(link) {
|
|
5397
5447
|
link.classList.remove("active");
|
|
5398
5448
|
if (link.getAttribute("href") === "#" + sectionId) {
|
|
@@ -5406,7 +5456,7 @@
|
|
|
5406
5456
|
state.resultsContainer.classList.add("is-open");
|
|
5407
5457
|
state.input.setAttribute("aria-expanded", "true");
|
|
5408
5458
|
if (typeof config.onOpen === "function") {
|
|
5409
|
-
config.onOpen
|
|
5459
|
+
safeInvokeCallback("onOpen", config.onOpen);
|
|
5410
5460
|
}
|
|
5411
5461
|
}
|
|
5412
5462
|
function close() {
|
|
@@ -5417,7 +5467,7 @@
|
|
|
5417
5467
|
state.input.setAttribute("aria-expanded", "false");
|
|
5418
5468
|
state.input.removeAttribute("aria-activedescendant");
|
|
5419
5469
|
if (typeof config.onClose === "function") {
|
|
5420
|
-
config.onClose
|
|
5470
|
+
safeInvokeCallback("onClose", config.onClose);
|
|
5421
5471
|
}
|
|
5422
5472
|
}
|
|
5423
5473
|
function destroy() {
|
|
@@ -5431,7 +5481,7 @@
|
|
|
5431
5481
|
clearTimeout(state.debounceTimer);
|
|
5432
5482
|
}
|
|
5433
5483
|
if (state.resultsContainer) {
|
|
5434
|
-
|
|
5484
|
+
setResultsHtml("");
|
|
5435
5485
|
}
|
|
5436
5486
|
}
|
|
5437
5487
|
function rebuild() {
|
|
@@ -5446,7 +5496,7 @@
|
|
|
5446
5496
|
function getIndex() {
|
|
5447
5497
|
return state.index.slice();
|
|
5448
5498
|
}
|
|
5449
|
-
|
|
5499
|
+
const instance = {
|
|
5450
5500
|
init,
|
|
5451
5501
|
destroy,
|
|
5452
5502
|
rebuild,
|
|
@@ -5459,12 +5509,12 @@
|
|
|
5459
5509
|
};
|
|
5460
5510
|
return instance;
|
|
5461
5511
|
}
|
|
5462
|
-
|
|
5512
|
+
const Search = {
|
|
5463
5513
|
// Factory method — creates and auto-initializes a new independent instance.
|
|
5464
5514
|
// Always returns the instance so callers retain a reference even if the
|
|
5465
5515
|
// DOM container is not yet available (they can retry init() later).
|
|
5466
5516
|
create: function(options) {
|
|
5467
|
-
|
|
5517
|
+
const instance = createSearch(options);
|
|
5468
5518
|
if (instance) {
|
|
5469
5519
|
instance.init();
|
|
5470
5520
|
}
|
|
@@ -5538,6 +5588,565 @@
|
|
|
5538
5588
|
window.VanduoDocSearch = Search;
|
|
5539
5589
|
})();
|
|
5540
5590
|
|
|
5591
|
+
// js/components/draggable.js
|
|
5592
|
+
(function() {
|
|
5593
|
+
"use strict";
|
|
5594
|
+
const Draggable = {
|
|
5595
|
+
// Store initialized draggables and their cleanup functions
|
|
5596
|
+
instances: /* @__PURE__ */ new Map(),
|
|
5597
|
+
// Store current drag state
|
|
5598
|
+
currentDrag: null,
|
|
5599
|
+
// Store touch state
|
|
5600
|
+
touchState: null,
|
|
5601
|
+
// Feedback element
|
|
5602
|
+
feedbackElement: null,
|
|
5603
|
+
/**
|
|
5604
|
+
* Initialize draggable components
|
|
5605
|
+
*/
|
|
5606
|
+
init: function() {
|
|
5607
|
+
const draggables = document.querySelectorAll(".vd-draggable, [data-draggable]");
|
|
5608
|
+
draggables.forEach((element) => {
|
|
5609
|
+
if (this.instances.has(element)) {
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
this.initDraggable(element);
|
|
5613
|
+
});
|
|
5614
|
+
const containers = document.querySelectorAll(".vd-draggable-container, .vd-draggable-container-vertical");
|
|
5615
|
+
containers.forEach((container) => {
|
|
5616
|
+
if (!this.instances.has(container)) {
|
|
5617
|
+
this.initContainer(container);
|
|
5618
|
+
}
|
|
5619
|
+
});
|
|
5620
|
+
const dropZones = document.querySelectorAll(".vd-drop-zone");
|
|
5621
|
+
dropZones.forEach((zone) => {
|
|
5622
|
+
if (!this.instances.has(zone)) {
|
|
5623
|
+
this.initDropZone(zone);
|
|
5624
|
+
}
|
|
5625
|
+
});
|
|
5626
|
+
this.createFeedbackElement();
|
|
5627
|
+
},
|
|
5628
|
+
/**
|
|
5629
|
+
* Initialize a single draggable element
|
|
5630
|
+
* @param {HTMLElement} element - Draggable element
|
|
5631
|
+
*/
|
|
5632
|
+
initDraggable: function(element) {
|
|
5633
|
+
const cleanupFunctions = [];
|
|
5634
|
+
if (!element.hasAttribute("draggable")) {
|
|
5635
|
+
element.setAttribute("draggable", "true");
|
|
5636
|
+
}
|
|
5637
|
+
if (!element.hasAttribute("tabindex")) {
|
|
5638
|
+
element.setAttribute("tabindex", "0");
|
|
5639
|
+
}
|
|
5640
|
+
element.setAttribute("role", "option");
|
|
5641
|
+
element.setAttribute("aria-roledescription", "draggable item");
|
|
5642
|
+
element.setAttribute("aria-grabbed", "false");
|
|
5643
|
+
const dragStartHandler = (e) => {
|
|
5644
|
+
this.handleDragStart(e, element);
|
|
5645
|
+
};
|
|
5646
|
+
element.addEventListener("dragstart", dragStartHandler);
|
|
5647
|
+
cleanupFunctions.push(() => element.removeEventListener("dragstart", dragStartHandler));
|
|
5648
|
+
const dragHandler = (e) => {
|
|
5649
|
+
this.handleDrag(e, element);
|
|
5650
|
+
};
|
|
5651
|
+
element.addEventListener("drag", dragHandler);
|
|
5652
|
+
cleanupFunctions.push(() => element.removeEventListener("drag", dragHandler));
|
|
5653
|
+
const dragEndHandler = (e) => {
|
|
5654
|
+
this.handleDragEnd(e, element);
|
|
5655
|
+
};
|
|
5656
|
+
element.addEventListener("dragend", dragEndHandler);
|
|
5657
|
+
cleanupFunctions.push(() => element.removeEventListener("dragend", dragEndHandler));
|
|
5658
|
+
const touchStartHandler = (e) => {
|
|
5659
|
+
this.handleTouchStart(e, element);
|
|
5660
|
+
};
|
|
5661
|
+
element.addEventListener("touchstart", touchStartHandler);
|
|
5662
|
+
cleanupFunctions.push(() => element.removeEventListener("touchstart", touchStartHandler));
|
|
5663
|
+
const touchMoveHandler = (e) => {
|
|
5664
|
+
this.handleTouchMove(e, element);
|
|
5665
|
+
};
|
|
5666
|
+
element.addEventListener("touchmove", touchMoveHandler);
|
|
5667
|
+
cleanupFunctions.push(() => element.removeEventListener("touchmove", touchMoveHandler));
|
|
5668
|
+
const touchEndHandler = (e) => {
|
|
5669
|
+
this.handleTouchEnd(e, element);
|
|
5670
|
+
};
|
|
5671
|
+
element.addEventListener("touchend", touchEndHandler);
|
|
5672
|
+
cleanupFunctions.push(() => element.removeEventListener("touchend", touchEndHandler));
|
|
5673
|
+
const touchCancelHandler = (e) => {
|
|
5674
|
+
this.handleTouchEnd(e, element);
|
|
5675
|
+
};
|
|
5676
|
+
element.addEventListener("touchcancel", touchCancelHandler);
|
|
5677
|
+
cleanupFunctions.push(() => element.removeEventListener("touchcancel", touchCancelHandler));
|
|
5678
|
+
const keydownHandler = (e) => {
|
|
5679
|
+
this.handleKeydown(e, element);
|
|
5680
|
+
};
|
|
5681
|
+
element.addEventListener("keydown", keydownHandler);
|
|
5682
|
+
cleanupFunctions.push(() => element.removeEventListener("keydown", keydownHandler));
|
|
5683
|
+
this.instances.set(element, { cleanup: cleanupFunctions });
|
|
5684
|
+
},
|
|
5685
|
+
/**
|
|
5686
|
+
* Initialize a draggable container
|
|
5687
|
+
* @param {HTMLElement} container - Draggable container
|
|
5688
|
+
*/
|
|
5689
|
+
initContainer: function(container) {
|
|
5690
|
+
container.setAttribute("role", "listbox");
|
|
5691
|
+
container.setAttribute("aria-label", container.getAttribute("aria-label") || "Draggable items");
|
|
5692
|
+
const items = container.querySelectorAll(".vd-draggable-item");
|
|
5693
|
+
items.forEach((item) => {
|
|
5694
|
+
if (!this.instances.has(item)) {
|
|
5695
|
+
this.initDraggable(item);
|
|
5696
|
+
}
|
|
5697
|
+
});
|
|
5698
|
+
const cleanupFunctions = [];
|
|
5699
|
+
const dragEnterHandler = (e) => {
|
|
5700
|
+
e.preventDefault();
|
|
5701
|
+
e.dataTransfer.dropEffect = "move";
|
|
5702
|
+
};
|
|
5703
|
+
const dragOverHandler = (e) => {
|
|
5704
|
+
e.preventDefault();
|
|
5705
|
+
e.dataTransfer.dropEffect = "move";
|
|
5706
|
+
if (!this.currentDrag) return;
|
|
5707
|
+
const draggingEl = this.currentDrag.element;
|
|
5708
|
+
if (!container.contains(draggingEl)) return;
|
|
5709
|
+
if (e.clientX === 0 && e.clientY === 0) return;
|
|
5710
|
+
this.handleReorder(container, draggingEl, e.clientX, e.clientY);
|
|
5711
|
+
};
|
|
5712
|
+
const dropHandler = (e) => {
|
|
5713
|
+
e.preventDefault();
|
|
5714
|
+
};
|
|
5715
|
+
container.addEventListener("dragenter", dragEnterHandler);
|
|
5716
|
+
container.addEventListener("dragover", dragOverHandler);
|
|
5717
|
+
container.addEventListener("drop", dropHandler);
|
|
5718
|
+
cleanupFunctions.push(() => {
|
|
5719
|
+
container.removeEventListener("dragenter", dragEnterHandler);
|
|
5720
|
+
container.removeEventListener("dragover", dragOverHandler);
|
|
5721
|
+
container.removeEventListener("drop", dropHandler);
|
|
5722
|
+
});
|
|
5723
|
+
this.instances.set(container, { cleanup: cleanupFunctions });
|
|
5724
|
+
},
|
|
5725
|
+
/**
|
|
5726
|
+
* Initialize a drop zone
|
|
5727
|
+
* @param {HTMLElement} zone - Drop zone element
|
|
5728
|
+
*/
|
|
5729
|
+
initDropZone: function(zone) {
|
|
5730
|
+
const cleanupFunctions = [];
|
|
5731
|
+
zone.setAttribute("role", "region");
|
|
5732
|
+
zone.setAttribute("aria-dropeffect", "move");
|
|
5733
|
+
if (!zone.hasAttribute("aria-label")) {
|
|
5734
|
+
zone.setAttribute("aria-label", "Drop zone");
|
|
5735
|
+
}
|
|
5736
|
+
const dragOverHandler = (e) => {
|
|
5737
|
+
e.preventDefault();
|
|
5738
|
+
this.handleDragOver(e, zone);
|
|
5739
|
+
};
|
|
5740
|
+
zone.addEventListener("dragover", dragOverHandler);
|
|
5741
|
+
cleanupFunctions.push(() => zone.removeEventListener("dragover", dragOverHandler));
|
|
5742
|
+
const dragEnterHandler = (e) => {
|
|
5743
|
+
e.preventDefault();
|
|
5744
|
+
this.handleDragEnter(e, zone);
|
|
5745
|
+
};
|
|
5746
|
+
zone.addEventListener("dragenter", dragEnterHandler);
|
|
5747
|
+
cleanupFunctions.push(() => zone.removeEventListener("dragenter", dragEnterHandler));
|
|
5748
|
+
const dragLeaveHandler = (e) => {
|
|
5749
|
+
this.handleDragLeave(e, zone);
|
|
5750
|
+
};
|
|
5751
|
+
zone.addEventListener("dragleave", dragLeaveHandler);
|
|
5752
|
+
cleanupFunctions.push(() => zone.removeEventListener("dragleave", dragLeaveHandler));
|
|
5753
|
+
const dropHandler = (e) => {
|
|
5754
|
+
e.preventDefault();
|
|
5755
|
+
this.handleDrop(e, zone);
|
|
5756
|
+
};
|
|
5757
|
+
zone.addEventListener("drop", dropHandler);
|
|
5758
|
+
cleanupFunctions.push(() => zone.removeEventListener("drop", dropHandler));
|
|
5759
|
+
this.instances.set(zone, { cleanup: cleanupFunctions });
|
|
5760
|
+
},
|
|
5761
|
+
/**
|
|
5762
|
+
* Create feedback element for drag operations
|
|
5763
|
+
*/
|
|
5764
|
+
createFeedbackElement: function() {
|
|
5765
|
+
if (!this.feedbackElement) {
|
|
5766
|
+
const existing = document.querySelector(".vd-drag-feedback");
|
|
5767
|
+
if (existing) {
|
|
5768
|
+
this.feedbackElement = existing;
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
this.feedbackElement = document.createElement("div");
|
|
5772
|
+
this.feedbackElement.className = "vd-drag-feedback hidden";
|
|
5773
|
+
this.feedbackElement.setAttribute("role", "presentation");
|
|
5774
|
+
document.body.appendChild(this.feedbackElement);
|
|
5775
|
+
}
|
|
5776
|
+
},
|
|
5777
|
+
/**
|
|
5778
|
+
* Handle drag start event
|
|
5779
|
+
* @param {DragEvent} e - Drag event
|
|
5780
|
+
* @param {HTMLElement} element - Draggable element
|
|
5781
|
+
*/
|
|
5782
|
+
handleDragStart: function(e, element) {
|
|
5783
|
+
element.classList.add("is-dragging");
|
|
5784
|
+
element.setAttribute("aria-grabbed", "true");
|
|
5785
|
+
this.currentDrag = {
|
|
5786
|
+
element,
|
|
5787
|
+
initialPosition: { x: e.clientX, y: e.clientY },
|
|
5788
|
+
initialBounds: element.getBoundingClientRect(),
|
|
5789
|
+
data: this.getData(element)
|
|
5790
|
+
};
|
|
5791
|
+
e.dataTransfer.effectAllowed = "move";
|
|
5792
|
+
e.dataTransfer.setData("text/plain", this.currentDrag.data);
|
|
5793
|
+
element.dispatchEvent(new CustomEvent("draggable:start", {
|
|
5794
|
+
bubbles: true,
|
|
5795
|
+
detail: {
|
|
5796
|
+
element,
|
|
5797
|
+
data: this.currentDrag.data,
|
|
5798
|
+
position: { x: e.clientX, y: e.clientY }
|
|
5799
|
+
}
|
|
5800
|
+
}));
|
|
5801
|
+
},
|
|
5802
|
+
/**
|
|
5803
|
+
* Handle drag event
|
|
5804
|
+
* @param {DragEvent} e - Drag event
|
|
5805
|
+
* @param {HTMLElement} element - Draggable element
|
|
5806
|
+
*/
|
|
5807
|
+
handleDrag: function(e, element) {
|
|
5808
|
+
if (!this.currentDrag) return;
|
|
5809
|
+
element.dispatchEvent(new CustomEvent("draggable:drag", {
|
|
5810
|
+
bubbles: true,
|
|
5811
|
+
detail: {
|
|
5812
|
+
element,
|
|
5813
|
+
data: this.currentDrag.data,
|
|
5814
|
+
position: { x: e.clientX, y: e.clientY },
|
|
5815
|
+
delta: {
|
|
5816
|
+
x: e.clientX - this.currentDrag.initialPosition.x,
|
|
5817
|
+
y: e.clientY - this.currentDrag.initialPosition.y
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
}));
|
|
5821
|
+
},
|
|
5822
|
+
/**
|
|
5823
|
+
* Handle drag end event
|
|
5824
|
+
* @param {DragEvent} e - Drag event
|
|
5825
|
+
* @param {HTMLElement} element - Draggable element
|
|
5826
|
+
*/
|
|
5827
|
+
handleDragEnd: function(e, element) {
|
|
5828
|
+
element.classList.remove("is-dragging");
|
|
5829
|
+
element.classList.add("is-dropped");
|
|
5830
|
+
setTimeout(() => element.classList.remove("is-dropped"), 300);
|
|
5831
|
+
element.setAttribute("aria-grabbed", "false");
|
|
5832
|
+
if (this.feedbackElement) {
|
|
5833
|
+
this.feedbackElement.classList.add("hidden");
|
|
5834
|
+
}
|
|
5835
|
+
const data = this.currentDrag?.data || this.getData(element);
|
|
5836
|
+
const initialPos = this.currentDrag?.initialPosition || { x: 0, y: 0 };
|
|
5837
|
+
element.dispatchEvent(new CustomEvent("draggable:end", {
|
|
5838
|
+
bubbles: true,
|
|
5839
|
+
detail: {
|
|
5840
|
+
element,
|
|
5841
|
+
data,
|
|
5842
|
+
position: { x: e.clientX, y: e.clientY },
|
|
5843
|
+
delta: {
|
|
5844
|
+
x: e.clientX - initialPos.x,
|
|
5845
|
+
y: e.clientY - initialPos.y
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
}));
|
|
5849
|
+
this.currentDrag = null;
|
|
5850
|
+
},
|
|
5851
|
+
/**
|
|
5852
|
+
* Handle touch start event (for mobile)
|
|
5853
|
+
* @param {TouchEvent} e - Touch event
|
|
5854
|
+
* @param {HTMLElement} element - Draggable element
|
|
5855
|
+
*/
|
|
5856
|
+
handleTouchStart: function(e, element) {
|
|
5857
|
+
const touch = e.touches[0];
|
|
5858
|
+
this.touchState = {
|
|
5859
|
+
element,
|
|
5860
|
+
startX: touch.clientX,
|
|
5861
|
+
startY: touch.clientY,
|
|
5862
|
+
startTime: Date.now(),
|
|
5863
|
+
isDragging: false
|
|
5864
|
+
};
|
|
5865
|
+
},
|
|
5866
|
+
/**
|
|
5867
|
+
* Handle touch move event (for mobile)
|
|
5868
|
+
* @param {TouchEvent} e - Touch event
|
|
5869
|
+
* @param {HTMLElement} element - Draggable element
|
|
5870
|
+
*/
|
|
5871
|
+
handleTouchMove: function(e, element) {
|
|
5872
|
+
if (!this.touchState) return;
|
|
5873
|
+
const touch = e.touches[0];
|
|
5874
|
+
const deltaX = touch.clientX - this.touchState.startX;
|
|
5875
|
+
const deltaY = touch.clientY - this.touchState.startY;
|
|
5876
|
+
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
|
5877
|
+
e.preventDefault();
|
|
5878
|
+
if (!this.touchState.isDragging) {
|
|
5879
|
+
this.touchState.isDragging = true;
|
|
5880
|
+
element.classList.add("is-dragging");
|
|
5881
|
+
element.setAttribute("aria-grabbed", "true");
|
|
5882
|
+
this.currentDrag = {
|
|
5883
|
+
element,
|
|
5884
|
+
initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
|
|
5885
|
+
initialBounds: element.getBoundingClientRect(),
|
|
5886
|
+
data: this.getData(element)
|
|
5887
|
+
};
|
|
5888
|
+
element.dispatchEvent(new CustomEvent("draggable:start", {
|
|
5889
|
+
bubbles: true,
|
|
5890
|
+
detail: {
|
|
5891
|
+
element,
|
|
5892
|
+
data: this.currentDrag.data,
|
|
5893
|
+
position: { x: touch.clientX, y: touch.clientY }
|
|
5894
|
+
}
|
|
5895
|
+
}));
|
|
5896
|
+
}
|
|
5897
|
+
this.updateFeedback(touch.clientX, touch.clientY);
|
|
5898
|
+
if (this.currentDrag) {
|
|
5899
|
+
element.dispatchEvent(new CustomEvent("draggable:drag", {
|
|
5900
|
+
bubbles: true,
|
|
5901
|
+
detail: {
|
|
5902
|
+
element,
|
|
5903
|
+
data: this.currentDrag.data,
|
|
5904
|
+
position: { x: touch.clientX, y: touch.clientY },
|
|
5905
|
+
delta: { x: deltaX, y: deltaY }
|
|
5906
|
+
}
|
|
5907
|
+
}));
|
|
5908
|
+
const container = element.closest(".vd-draggable-container");
|
|
5909
|
+
if (container && container.contains(element)) {
|
|
5910
|
+
this.handleReorder(container, element, touch.clientX, touch.clientY);
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
},
|
|
5915
|
+
/**
|
|
5916
|
+
* Handle touch end event (for mobile)
|
|
5917
|
+
* @param {TouchEvent} e - Touch event
|
|
5918
|
+
* @param {HTMLElement} element - Draggable element
|
|
5919
|
+
*/
|
|
5920
|
+
handleTouchEnd: function(e, element) {
|
|
5921
|
+
if (this.touchState && this.touchState.isDragging) {
|
|
5922
|
+
e.preventDefault();
|
|
5923
|
+
element.classList.remove("is-dragging");
|
|
5924
|
+
element.classList.add("is-dropped");
|
|
5925
|
+
element.setAttribute("aria-grabbed", "false");
|
|
5926
|
+
setTimeout(() => element.classList.remove("is-dropped"), 300);
|
|
5927
|
+
if (this.feedbackElement) {
|
|
5928
|
+
this.feedbackElement.classList.add("hidden");
|
|
5929
|
+
}
|
|
5930
|
+
const endTouch = e.changedTouches[0];
|
|
5931
|
+
const data = this.currentDrag?.data || this.getData(element);
|
|
5932
|
+
const startX = this.touchState?.startX || 0;
|
|
5933
|
+
const startY = this.touchState?.startY || 0;
|
|
5934
|
+
element.dispatchEvent(new CustomEvent("draggable:end", {
|
|
5935
|
+
bubbles: true,
|
|
5936
|
+
detail: {
|
|
5937
|
+
element,
|
|
5938
|
+
data,
|
|
5939
|
+
position: { x: endTouch.clientX, y: endTouch.clientY },
|
|
5940
|
+
delta: {
|
|
5941
|
+
x: endTouch.clientX - startX,
|
|
5942
|
+
y: endTouch.clientY - startY
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
}));
|
|
5946
|
+
}
|
|
5947
|
+
this.touchState = null;
|
|
5948
|
+
this.currentDrag = null;
|
|
5949
|
+
},
|
|
5950
|
+
/**
|
|
5951
|
+
* Handle drag over event
|
|
5952
|
+
* @param {DragEvent} e - Drag event
|
|
5953
|
+
* @param {HTMLElement} _zone - Drop zone element
|
|
5954
|
+
*/
|
|
5955
|
+
handleDragOver: function(e, _zone) {
|
|
5956
|
+
e.preventDefault();
|
|
5957
|
+
e.dataTransfer.dropEffect = "move";
|
|
5958
|
+
},
|
|
5959
|
+
/**
|
|
5960
|
+
* Handle drag enter event
|
|
5961
|
+
* @param {DragEvent} e - Drag event
|
|
5962
|
+
* @param {HTMLElement} zone - Drop zone element
|
|
5963
|
+
*/
|
|
5964
|
+
handleDragEnter: function(e, zone) {
|
|
5965
|
+
e.preventDefault();
|
|
5966
|
+
zone.classList.add("is-drag-over");
|
|
5967
|
+
},
|
|
5968
|
+
/**
|
|
5969
|
+
* Handle drag leave event
|
|
5970
|
+
* @param {DragEvent} e - Drag event
|
|
5971
|
+
* @param {HTMLElement} zone - Drop zone element
|
|
5972
|
+
*/
|
|
5973
|
+
handleDragLeave: function(e, zone) {
|
|
5974
|
+
zone.classList.remove("is-drag-over");
|
|
5975
|
+
},
|
|
5976
|
+
/**
|
|
5977
|
+
* Handle drop event
|
|
5978
|
+
* @param {DragEvent} e - Drag event
|
|
5979
|
+
* @param {HTMLElement} zone - Drop zone element
|
|
5980
|
+
*/
|
|
5981
|
+
handleDrop: function(e, zone) {
|
|
5982
|
+
e.preventDefault();
|
|
5983
|
+
zone.classList.remove("is-drag-over");
|
|
5984
|
+
zone.dispatchEvent(new CustomEvent("draggable:drop", {
|
|
5985
|
+
bubbles: true,
|
|
5986
|
+
detail: {
|
|
5987
|
+
zone,
|
|
5988
|
+
element: this.currentDrag?.element,
|
|
5989
|
+
data: this.currentDrag?.data,
|
|
5990
|
+
position: { x: e.clientX, y: e.clientY }
|
|
5991
|
+
}
|
|
5992
|
+
}));
|
|
5993
|
+
},
|
|
5994
|
+
/**
|
|
5995
|
+
* Reorder elements in container based on cursor position
|
|
5996
|
+
* @param {HTMLElement} container
|
|
5997
|
+
* @param {HTMLElement} element
|
|
5998
|
+
* @param {number} clientX
|
|
5999
|
+
* @param {number} clientY
|
|
6000
|
+
*/
|
|
6001
|
+
handleReorder: function(container, element, clientX, clientY) {
|
|
6002
|
+
const isVertical = container.classList.contains("vd-draggable-container-vertical");
|
|
6003
|
+
const siblings = [...container.querySelectorAll(".vd-draggable-item:not(.is-dragging), .vd-draggable:not(.is-dragging)")];
|
|
6004
|
+
const nextSibling = siblings.reduce((closest, child) => {
|
|
6005
|
+
const box = child.getBoundingClientRect();
|
|
6006
|
+
const offset = isVertical ? clientY - box.top - box.height / 2 : clientX - box.left - box.width / 2;
|
|
6007
|
+
if (offset < 0 && offset > closest.offset) {
|
|
6008
|
+
return { offset, element: child };
|
|
6009
|
+
} else {
|
|
6010
|
+
return closest;
|
|
6011
|
+
}
|
|
6012
|
+
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
|
6013
|
+
if (nextSibling == null) {
|
|
6014
|
+
container.appendChild(element);
|
|
6015
|
+
} else {
|
|
6016
|
+
container.insertBefore(element, nextSibling);
|
|
6017
|
+
}
|
|
6018
|
+
},
|
|
6019
|
+
/**
|
|
6020
|
+
* Handle keyboard events
|
|
6021
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
6022
|
+
* @param {HTMLElement} element - Draggable element
|
|
6023
|
+
*/
|
|
6024
|
+
handleKeydown: function(e, element) {
|
|
6025
|
+
switch (e.key) {
|
|
6026
|
+
case "Enter":
|
|
6027
|
+
case " ":
|
|
6028
|
+
e.preventDefault();
|
|
6029
|
+
element.click();
|
|
6030
|
+
break;
|
|
6031
|
+
case "Escape":
|
|
6032
|
+
if (element.classList.contains("is-dragging")) {
|
|
6033
|
+
element.classList.remove("is-dragging");
|
|
6034
|
+
element.setAttribute("aria-grabbed", "false");
|
|
6035
|
+
if (this.feedbackElement) {
|
|
6036
|
+
this.feedbackElement.classList.add("hidden");
|
|
6037
|
+
}
|
|
6038
|
+
this.currentDrag = null;
|
|
6039
|
+
}
|
|
6040
|
+
break;
|
|
6041
|
+
case "ArrowUp":
|
|
6042
|
+
case "ArrowLeft": {
|
|
6043
|
+
e.preventDefault();
|
|
6044
|
+
const prev = element.previousElementSibling;
|
|
6045
|
+
if (prev && (prev.classList.contains("vd-draggable") || prev.classList.contains("vd-draggable-item"))) {
|
|
6046
|
+
element.parentNode.insertBefore(element, prev);
|
|
6047
|
+
element.focus();
|
|
6048
|
+
element.dispatchEvent(new CustomEvent("draggable:reorder", {
|
|
6049
|
+
bubbles: true,
|
|
6050
|
+
detail: { element, direction: "up" }
|
|
6051
|
+
}));
|
|
6052
|
+
}
|
|
6053
|
+
break;
|
|
6054
|
+
}
|
|
6055
|
+
case "ArrowDown":
|
|
6056
|
+
case "ArrowRight": {
|
|
6057
|
+
e.preventDefault();
|
|
6058
|
+
const next = element.nextElementSibling;
|
|
6059
|
+
if (next && (next.classList.contains("vd-draggable") || next.classList.contains("vd-draggable-item"))) {
|
|
6060
|
+
element.parentNode.insertBefore(next, element);
|
|
6061
|
+
element.focus();
|
|
6062
|
+
element.dispatchEvent(new CustomEvent("draggable:reorder", {
|
|
6063
|
+
bubbles: true,
|
|
6064
|
+
detail: { element, direction: "down" }
|
|
6065
|
+
}));
|
|
6066
|
+
}
|
|
6067
|
+
break;
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
},
|
|
6071
|
+
/**
|
|
6072
|
+
* Get data from draggable element
|
|
6073
|
+
* @param {HTMLElement} element - Draggable element
|
|
6074
|
+
* @returns {string} Data associated with the element
|
|
6075
|
+
*/
|
|
6076
|
+
getData: function(element) {
|
|
6077
|
+
return element.dataset.draggable || element.textContent.trim();
|
|
6078
|
+
},
|
|
6079
|
+
/**
|
|
6080
|
+
* Update drag feedback element
|
|
6081
|
+
* @param {number} x - Current X coordinate
|
|
6082
|
+
* @param {number} y - Current Y coordinate
|
|
6083
|
+
*/
|
|
6084
|
+
updateFeedback: function(x, y) {
|
|
6085
|
+
if (!this.currentDrag) return;
|
|
6086
|
+
this.feedbackElement.classList.remove("hidden");
|
|
6087
|
+
const rect = this.currentDrag.initialBounds;
|
|
6088
|
+
this.feedbackElement.innerHTML = "";
|
|
6089
|
+
const clone = this.currentDrag.element.cloneNode(true);
|
|
6090
|
+
this.feedbackElement.appendChild(clone);
|
|
6091
|
+
Object.assign(this.feedbackElement.style, {
|
|
6092
|
+
left: x - 20 + "px",
|
|
6093
|
+
top: y - 20 + "px",
|
|
6094
|
+
width: rect.width + "px",
|
|
6095
|
+
height: rect.height + "px"
|
|
6096
|
+
});
|
|
6097
|
+
},
|
|
6098
|
+
/**
|
|
6099
|
+
* Make an element draggable programmatically
|
|
6100
|
+
* @param {HTMLElement|string} element - Element or selector
|
|
6101
|
+
* @param {Object} options - Configuration options
|
|
6102
|
+
*/
|
|
6103
|
+
makeDraggable: function(element, options = {}) {
|
|
6104
|
+
const el = typeof element === "string" ? document.querySelector(element) : element;
|
|
6105
|
+
if (el && !this.instances.has(el)) {
|
|
6106
|
+
el.classList.add("vd-draggable");
|
|
6107
|
+
el.setAttribute("draggable", "true");
|
|
6108
|
+
if (options.data) {
|
|
6109
|
+
el.dataset.draggable = options.data;
|
|
6110
|
+
}
|
|
6111
|
+
this.initDraggable(el);
|
|
6112
|
+
}
|
|
6113
|
+
},
|
|
6114
|
+
/**
|
|
6115
|
+
* Remove draggable functionality from an element
|
|
6116
|
+
* @param {HTMLElement|string} element - Element or selector
|
|
6117
|
+
*/
|
|
6118
|
+
removeDraggable: function(element) {
|
|
6119
|
+
const el = typeof element === "string" ? document.querySelector(element) : element;
|
|
6120
|
+
if (el && this.instances.has(el)) {
|
|
6121
|
+
const instance = this.instances.get(el);
|
|
6122
|
+
instance.cleanup.forEach((fn) => fn());
|
|
6123
|
+
this.instances.delete(el);
|
|
6124
|
+
el.classList.remove("vd-draggable");
|
|
6125
|
+
el.removeAttribute("draggable");
|
|
6126
|
+
el.removeAttribute("data-draggable");
|
|
6127
|
+
}
|
|
6128
|
+
},
|
|
6129
|
+
/**
|
|
6130
|
+
* Destroy a draggable instance and clean up event listeners
|
|
6131
|
+
* @param {HTMLElement} element - Draggable element
|
|
6132
|
+
*/
|
|
6133
|
+
destroy: function(element) {
|
|
6134
|
+
this.removeDraggable(element);
|
|
6135
|
+
},
|
|
6136
|
+
/**
|
|
6137
|
+
* Destroy all draggable instances
|
|
6138
|
+
*/
|
|
6139
|
+
destroyAll: function() {
|
|
6140
|
+
const instances = Array.from(this.instances.keys());
|
|
6141
|
+
instances.forEach((element) => this.destroy(element));
|
|
6142
|
+
}
|
|
6143
|
+
};
|
|
6144
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
6145
|
+
window.Vanduo.register("draggable", Draggable);
|
|
6146
|
+
}
|
|
6147
|
+
window.VanduoDraggable = Draggable;
|
|
6148
|
+
})();
|
|
6149
|
+
|
|
5541
6150
|
// js/index.js
|
|
5542
6151
|
var Vanduo = window.Vanduo;
|
|
5543
6152
|
var index_default = Vanduo;
|