@vortexm/vjt 0.1.5 → 0.1.6
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/index.js +324 -38
- package/dist/lib/action-runtime.d.ts +9 -0
- package/dist/lib/dom-state.d.ts +4 -0
- package/dist/lib/widgets/confirm-modal.d.ts +7 -0
- package/dist/lib/widgets/context-menu.d.ts +12 -6
- package/dist/lib/widgets/context.d.ts +2 -6
- package/dist/lib/widgets/delegation.d.ts +10 -0
- package/package.json +1 -1
- package/vjt-styles.css +63 -5
package/dist/index.js
CHANGED
|
@@ -4582,6 +4582,9 @@ var ActionRuntime = class {
|
|
|
4582
4582
|
focusWidget;
|
|
4583
4583
|
playAudio;
|
|
4584
4584
|
stopPlaying;
|
|
4585
|
+
copyToClipboard;
|
|
4586
|
+
selectFile;
|
|
4587
|
+
confirmModal;
|
|
4585
4588
|
startRecording;
|
|
4586
4589
|
stopRecording;
|
|
4587
4590
|
startListening;
|
|
@@ -4616,6 +4619,9 @@ var ActionRuntime = class {
|
|
|
4616
4619
|
this.focusWidget = options.focusWidget;
|
|
4617
4620
|
this.playAudio = options.playAudio;
|
|
4618
4621
|
this.stopPlaying = options.stopPlaying;
|
|
4622
|
+
this.copyToClipboard = options.copyToClipboard;
|
|
4623
|
+
this.selectFile = options.selectFile;
|
|
4624
|
+
this.confirmModal = options.confirmModal;
|
|
4619
4625
|
this.startRecording = options.startRecording;
|
|
4620
4626
|
this.stopRecording = options.stopRecording;
|
|
4621
4627
|
this.startListening = options.startListening;
|
|
@@ -4654,7 +4660,7 @@ var ActionRuntime = class {
|
|
|
4654
4660
|
async runActions(actions, inputValue, context) {
|
|
4655
4661
|
let current = inputValue;
|
|
4656
4662
|
for (const action of actions) {
|
|
4657
|
-
current = await this.runSingleAction(action,
|
|
4663
|
+
current = await this.runSingleAction(action, inputValue, context);
|
|
4658
4664
|
}
|
|
4659
4665
|
return current;
|
|
4660
4666
|
}
|
|
@@ -4666,32 +4672,7 @@ var ActionRuntime = class {
|
|
|
4666
4672
|
try {
|
|
4667
4673
|
let output = await this.executeAction(action, inputValue, context);
|
|
4668
4674
|
if (action.andThen?.length) {
|
|
4669
|
-
if (
|
|
4670
|
-
const collected = [];
|
|
4671
|
-
for (const [index, entry] of output.entries()) {
|
|
4672
|
-
if (entry === null || entry === void 0) {
|
|
4673
|
-
continue;
|
|
4674
|
-
}
|
|
4675
|
-
const nestedOutput = await this.runActions(action.andThen, entry, {
|
|
4676
|
-
...context,
|
|
4677
|
-
currentValue: entry,
|
|
4678
|
-
currentList: output,
|
|
4679
|
-
currentIndex: index
|
|
4680
|
-
});
|
|
4681
|
-
if (nestedOutput !== null && nestedOutput !== void 0) {
|
|
4682
|
-
if (Array.isArray(nestedOutput)) {
|
|
4683
|
-
for (const item of nestedOutput) {
|
|
4684
|
-
if (item !== null && item !== void 0) {
|
|
4685
|
-
collected.push(item);
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
} else {
|
|
4689
|
-
collected.push(nestedOutput);
|
|
4690
|
-
}
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
output = collected;
|
|
4694
|
-
} else if (output !== null && output !== void 0) {
|
|
4675
|
+
if (output !== null && output !== void 0) {
|
|
4695
4676
|
output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
|
|
4696
4677
|
} else {
|
|
4697
4678
|
output = null;
|
|
@@ -4755,6 +4736,16 @@ var ActionRuntime = class {
|
|
|
4755
4736
|
await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
|
|
4756
4737
|
return null;
|
|
4757
4738
|
}
|
|
4739
|
+
if (name === "copyToClipboard") {
|
|
4740
|
+
await this.copyToClipboard(inputValue);
|
|
4741
|
+
return inputValue;
|
|
4742
|
+
}
|
|
4743
|
+
if (name === "selectFile") {
|
|
4744
|
+
return this.selectFile(action.args);
|
|
4745
|
+
}
|
|
4746
|
+
if (name === "confirmModal") {
|
|
4747
|
+
return await this.confirmModal(action.args, inputValue) ? inputValue : void 0;
|
|
4748
|
+
}
|
|
4758
4749
|
if (name === "stopPlaying") {
|
|
4759
4750
|
await this.stopPlaying();
|
|
4760
4751
|
return null;
|
|
@@ -4785,7 +4776,9 @@ var ActionRuntime = class {
|
|
|
4785
4776
|
this.setActiveContextMenu({
|
|
4786
4777
|
id: menuId,
|
|
4787
4778
|
x: position.x,
|
|
4788
|
-
y: position.y
|
|
4779
|
+
y: position.y,
|
|
4780
|
+
currentValue: context.currentValue,
|
|
4781
|
+
openPath: []
|
|
4789
4782
|
});
|
|
4790
4783
|
return null;
|
|
4791
4784
|
}
|
|
@@ -4827,7 +4820,11 @@ var ActionRuntime = class {
|
|
|
4827
4820
|
return inputValue;
|
|
4828
4821
|
}
|
|
4829
4822
|
if (name.startsWith("get ")) {
|
|
4830
|
-
|
|
4823
|
+
const reference = name.slice(4);
|
|
4824
|
+
if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
|
|
4825
|
+
return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
|
|
4826
|
+
}
|
|
4827
|
+
return this.resolveReference(reference, context.currentValue, context.responseValue);
|
|
4831
4828
|
}
|
|
4832
4829
|
if (name.startsWith("equals ")) {
|
|
4833
4830
|
return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
|
|
@@ -4848,7 +4845,11 @@ var ActionRuntime = class {
|
|
|
4848
4845
|
return nextValue;
|
|
4849
4846
|
}
|
|
4850
4847
|
if (name.startsWith("filter ")) {
|
|
4851
|
-
const
|
|
4848
|
+
const reference = name.slice(7);
|
|
4849
|
+
if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
|
|
4850
|
+
return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
|
|
4851
|
+
}
|
|
4852
|
+
const value = this.resolveReference(reference, context.currentValue, context.responseValue);
|
|
4852
4853
|
return value ? inputValue : null;
|
|
4853
4854
|
}
|
|
4854
4855
|
if (name === "remove") {
|
|
@@ -4909,6 +4910,9 @@ var ActionRuntime = class {
|
|
|
4909
4910
|
return inputValue;
|
|
4910
4911
|
}
|
|
4911
4912
|
if (name === "map") {
|
|
4913
|
+
if (Array.isArray(context.currentValue)) {
|
|
4914
|
+
return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
|
|
4915
|
+
}
|
|
4912
4916
|
return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
|
|
4913
4917
|
}
|
|
4914
4918
|
if (name === "ifelse") {
|
|
@@ -4951,6 +4955,9 @@ var ActionRuntime = class {
|
|
|
4951
4955
|
}
|
|
4952
4956
|
return JSON.stringify(value);
|
|
4953
4957
|
}
|
|
4958
|
+
isCurrentScopedReference(reference) {
|
|
4959
|
+
return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
|
|
4960
|
+
}
|
|
4954
4961
|
};
|
|
4955
4962
|
function isIfElseArgs(value) {
|
|
4956
4963
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -6324,6 +6331,21 @@ function renderModalOverlay(env, modalId) {
|
|
|
6324
6331
|
}
|
|
6325
6332
|
|
|
6326
6333
|
// src/lib/widgets/context-menu.ts
|
|
6334
|
+
var MENU_WIDTH = 220;
|
|
6335
|
+
var MENU_ITEM_HEIGHT = 44;
|
|
6336
|
+
var MENU_PADDING = 8;
|
|
6337
|
+
function getContextMenuItemAtPath(items, path) {
|
|
6338
|
+
let currentItems = items ?? [];
|
|
6339
|
+
let currentItem = null;
|
|
6340
|
+
for (const index of path) {
|
|
6341
|
+
currentItem = currentItems[index] ?? null;
|
|
6342
|
+
if (!currentItem) {
|
|
6343
|
+
return null;
|
|
6344
|
+
}
|
|
6345
|
+
currentItems = currentItem.items ?? [];
|
|
6346
|
+
}
|
|
6347
|
+
return currentItem;
|
|
6348
|
+
}
|
|
6327
6349
|
function renderContextMenuOverlay(env, menu) {
|
|
6328
6350
|
const node = env.nodeById.get(menu.id);
|
|
6329
6351
|
if (!node || node.widget !== "context-menu") {
|
|
@@ -6331,8 +6353,52 @@ function renderContextMenuOverlay(env, menu) {
|
|
|
6331
6353
|
}
|
|
6332
6354
|
const state = env.ensureWidgetState(node, menu.id);
|
|
6333
6355
|
const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
|
|
6334
|
-
const
|
|
6335
|
-
|
|
6356
|
+
const openPath = Array.isArray(menu.openPath) ? menu.openPath : [];
|
|
6357
|
+
const layers = [];
|
|
6358
|
+
let items = node.items ?? [];
|
|
6359
|
+
let pathPrefix = [];
|
|
6360
|
+
let layerX = menu.x;
|
|
6361
|
+
let layerY = menu.y;
|
|
6362
|
+
while (items.length > 0) {
|
|
6363
|
+
layers.push(renderMenuLayer(env, menu.id, items, enabled, layerX, layerY, pathPrefix));
|
|
6364
|
+
const nextIndex = openPath[pathPrefix.length];
|
|
6365
|
+
if (typeof nextIndex !== "number") {
|
|
6366
|
+
break;
|
|
6367
|
+
}
|
|
6368
|
+
const nextItem = items[nextIndex];
|
|
6369
|
+
if (!nextItem?.items?.length) {
|
|
6370
|
+
break;
|
|
6371
|
+
}
|
|
6372
|
+
const proposedY = layerY + MENU_PADDING + nextIndex * MENU_ITEM_HEIGHT;
|
|
6373
|
+
const rightX = layerX + MENU_WIDTH - MENU_PADDING;
|
|
6374
|
+
const leftX = layerX - MENU_WIDTH + MENU_PADDING;
|
|
6375
|
+
layerX = rightX + MENU_WIDTH <= window.innerWidth ? rightX : Math.max(8, leftX);
|
|
6376
|
+
layerY = Math.max(8, Math.min(proposedY, window.innerHeight - (16 + nextItem.items.length * MENU_ITEM_HEIGHT) - 8));
|
|
6377
|
+
items = nextItem.items;
|
|
6378
|
+
pathPrefix = [...pathPrefix, nextIndex];
|
|
6379
|
+
}
|
|
6380
|
+
return `<div class="vjt-context-menu-backdrop" data-context-menu-backdrop="${env.escapeHtml(menu.id)}">${layers.join("")}</div>`;
|
|
6381
|
+
}
|
|
6382
|
+
function renderMenuLayer(env, menuId, items, enabled, x, y, pathPrefix) {
|
|
6383
|
+
const content = items.map((item, index) => {
|
|
6384
|
+
const path = [...pathPrefix, index];
|
|
6385
|
+
const pathText = path.join(".");
|
|
6386
|
+
const label = env.escapeHtml(env.resolveI18nValue(item.name));
|
|
6387
|
+
const disabled = enabled ? "" : " disabled";
|
|
6388
|
+
if (item.items?.length) {
|
|
6389
|
+
return `<button class="vjt-context-menu-item vjt-context-menu-item--parent" type="button" data-context-menu-expand="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}><span>${label}</span><span class="vjt-context-menu-chevron">></span></button>`;
|
|
6390
|
+
}
|
|
6391
|
+
return `<button class="vjt-context-menu-item" type="button" data-context-menu-item="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}>${label}</button>`;
|
|
6392
|
+
}).join("");
|
|
6393
|
+
return `<div class="vjt-context-menu" style="left:${x}px;top:${y}px">${content}</div>`;
|
|
6394
|
+
}
|
|
6395
|
+
|
|
6396
|
+
// src/lib/widgets/confirm-modal.ts
|
|
6397
|
+
function renderConfirmModalOverlay(env, modal) {
|
|
6398
|
+
const caption = env.escapeHtml(env.resolveI18nValue(modal.caption));
|
|
6399
|
+
const yes = env.escapeHtml(env.resolveI18nValue(modal.yes));
|
|
6400
|
+
const no = env.escapeHtml(env.resolveI18nValue(modal.no));
|
|
6401
|
+
return `<div class="vjt-confirm-backdrop" data-confirm-backdrop="true"><div class="vjt-confirm-window"><div class="vjt-confirm-caption">${caption}</div><div class="vjt-confirm-actions"><button class="vjt-button vjt-button--bright" type="button" data-confirm-yes="true">${yes}</button><button class="vjt-button vjt-button--regular" type="button" data-confirm-no="true">${no}</button></div></div></div>`;
|
|
6336
6402
|
}
|
|
6337
6403
|
|
|
6338
6404
|
// src/lib/widgets/bindings.ts
|
|
@@ -6524,7 +6590,7 @@ function bindWidgetEvents(root, env) {
|
|
|
6524
6590
|
});
|
|
6525
6591
|
continue;
|
|
6526
6592
|
}
|
|
6527
|
-
if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && env.getInlineActions(node)?.length) {
|
|
6593
|
+
if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && (node.events?.onClick?.length || env.getInlineActions(node)?.length)) {
|
|
6528
6594
|
element.addEventListener("pointerdown", (event) => {
|
|
6529
6595
|
env.setLastPointer({ x: event.clientX, y: event.clientY });
|
|
6530
6596
|
});
|
|
@@ -6594,6 +6660,45 @@ function bindDelegatedUi(root, env) {
|
|
|
6594
6660
|
window.addEventListener("pointercancel", () => {
|
|
6595
6661
|
env.stopSplitterDrag();
|
|
6596
6662
|
});
|
|
6663
|
+
root.addEventListener("pointerover", (event) => {
|
|
6664
|
+
void (async () => {
|
|
6665
|
+
const target = event.target;
|
|
6666
|
+
if (!(target instanceof Element)) {
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
const hoveredItem = target.closest("[data-context-menu-item], [data-context-menu-expand]");
|
|
6670
|
+
if (!(hoveredItem instanceof HTMLButtonElement)) {
|
|
6671
|
+
return;
|
|
6672
|
+
}
|
|
6673
|
+
const descriptor = hoveredItem.dataset.contextMenuExpand ?? hoveredItem.dataset.contextMenuItem;
|
|
6674
|
+
if (!descriptor) {
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
6677
|
+
const [menuId, pathText = ""] = descriptor.split(":");
|
|
6678
|
+
const node = env.nodeById.get(menuId);
|
|
6679
|
+
if (node?.widget !== "context-menu") {
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6682
|
+
const activeMenu = env.getActiveContextMenu();
|
|
6683
|
+
if (!activeMenu || activeMenu.id !== menuId) {
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
const path = parseMenuPath(pathText);
|
|
6687
|
+
const item = getContextMenuItemAtPath(node.items, path);
|
|
6688
|
+
if (!item) {
|
|
6689
|
+
return;
|
|
6690
|
+
}
|
|
6691
|
+
const nextOpenPath = item.items?.length ? path : path.slice(0, -1);
|
|
6692
|
+
if (samePath(activeMenu.openPath ?? [], nextOpenPath)) {
|
|
6693
|
+
return;
|
|
6694
|
+
}
|
|
6695
|
+
env.setActiveContextMenu({
|
|
6696
|
+
...activeMenu,
|
|
6697
|
+
openPath: nextOpenPath
|
|
6698
|
+
});
|
|
6699
|
+
await env.rerenderRoot();
|
|
6700
|
+
})();
|
|
6701
|
+
});
|
|
6597
6702
|
root.addEventListener("click", (event) => {
|
|
6598
6703
|
void (async () => {
|
|
6599
6704
|
const target = event.target;
|
|
@@ -6672,7 +6777,7 @@ function bindDelegatedUi(root, env) {
|
|
|
6672
6777
|
}
|
|
6673
6778
|
const menuItem = target.closest("[data-context-menu-item]");
|
|
6674
6779
|
if (menuItem instanceof HTMLButtonElement) {
|
|
6675
|
-
const [menuId,
|
|
6780
|
+
const [menuId, pathText = ""] = (menuItem.dataset.contextMenuItem ?? "").split(":");
|
|
6676
6781
|
const node = env.nodeById.get(menuId);
|
|
6677
6782
|
if (node?.widget !== "context-menu") {
|
|
6678
6783
|
return;
|
|
@@ -6680,11 +6785,27 @@ function bindDelegatedUi(root, env) {
|
|
|
6680
6785
|
if (!env.isWidgetEnabled(node, menuId)) {
|
|
6681
6786
|
return;
|
|
6682
6787
|
}
|
|
6683
|
-
const item = node.items
|
|
6788
|
+
const item = getContextMenuItemAtPath(node.items, parseMenuPath(pathText));
|
|
6789
|
+
const menuContext = env.getActiveContextMenu();
|
|
6790
|
+
const currentValue = menuContext?.id === menuId ? menuContext.currentValue ?? null : null;
|
|
6684
6791
|
env.setActiveContextMenu(null);
|
|
6685
6792
|
if (item?.actions?.length) {
|
|
6686
|
-
await env.runActions(item.actions,
|
|
6793
|
+
await env.runActions(item.actions, currentValue, { currentValue });
|
|
6794
|
+
}
|
|
6795
|
+
await env.rerenderRoot();
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
const menuParentItem = target.closest("[data-context-menu-expand]");
|
|
6799
|
+
if (menuParentItem instanceof HTMLButtonElement) {
|
|
6800
|
+
const [menuId, pathText = ""] = (menuParentItem.dataset.contextMenuExpand ?? "").split(":");
|
|
6801
|
+
const activeMenu = env.getActiveContextMenu();
|
|
6802
|
+
if (!activeMenu || activeMenu.id !== menuId) {
|
|
6803
|
+
return;
|
|
6687
6804
|
}
|
|
6805
|
+
env.setActiveContextMenu({
|
|
6806
|
+
...activeMenu,
|
|
6807
|
+
openPath: parseMenuPath(pathText)
|
|
6808
|
+
});
|
|
6688
6809
|
await env.rerenderRoot();
|
|
6689
6810
|
return;
|
|
6690
6811
|
}
|
|
@@ -6703,10 +6824,32 @@ function bindDelegatedUi(root, env) {
|
|
|
6703
6824
|
await env.runActions(item.actions, null, { currentValue: null });
|
|
6704
6825
|
}
|
|
6705
6826
|
await env.rerenderRoot();
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
if (target.closest("[data-confirm-yes]")) {
|
|
6830
|
+
env.resolveConfirmModal(true);
|
|
6831
|
+
await env.rerenderRoot();
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
if (target.closest("[data-confirm-no]")) {
|
|
6835
|
+
env.resolveConfirmModal(false);
|
|
6836
|
+
await env.rerenderRoot();
|
|
6706
6837
|
}
|
|
6707
6838
|
})();
|
|
6708
6839
|
});
|
|
6709
6840
|
}
|
|
6841
|
+
function parseMenuPath(value) {
|
|
6842
|
+
if (!value) {
|
|
6843
|
+
return [];
|
|
6844
|
+
}
|
|
6845
|
+
return value.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => !Number.isNaN(part));
|
|
6846
|
+
}
|
|
6847
|
+
function samePath(left, right) {
|
|
6848
|
+
if (left.length !== right.length) {
|
|
6849
|
+
return false;
|
|
6850
|
+
}
|
|
6851
|
+
return left.every((value, index) => value === right[index]);
|
|
6852
|
+
}
|
|
6710
6853
|
|
|
6711
6854
|
// src/lib/render.ts
|
|
6712
6855
|
var DEFAULT_DATE_FORMAT = "dd.MM.yyyy";
|
|
@@ -6863,6 +7006,7 @@ var RuntimeRenderer = class {
|
|
|
6863
7006
|
hasTriggeredInitialRefresh = false;
|
|
6864
7007
|
activeModalId = null;
|
|
6865
7008
|
activeContextMenu = null;
|
|
7009
|
+
activeConfirmModal = null;
|
|
6866
7010
|
lastPointer = { x: 24, y: 24 };
|
|
6867
7011
|
pendingResetModalIds = /* @__PURE__ */ new Set();
|
|
6868
7012
|
constructor(description, options = {}) {
|
|
@@ -6966,6 +7110,9 @@ var RuntimeRenderer = class {
|
|
|
6966
7110
|
focusWidget: (reference) => this.focusWidget(reference),
|
|
6967
7111
|
playAudio: (value) => this.voiceRuntime.play(value),
|
|
6968
7112
|
stopPlaying: () => this.voiceRuntime.stopPlaying(),
|
|
7113
|
+
copyToClipboard: (value) => this.copyToClipboard(value),
|
|
7114
|
+
selectFile: (args) => this.selectFile(args),
|
|
7115
|
+
confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
|
|
6969
7116
|
startRecording: () => this.voiceRuntime.startRecording(),
|
|
6970
7117
|
stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
|
|
6971
7118
|
startListening: () => this.voiceRuntime.startListening(),
|
|
@@ -7124,6 +7271,138 @@ var RuntimeRenderer = class {
|
|
|
7124
7271
|
await this.rerenderRoot();
|
|
7125
7272
|
await this.runSystemEvent("onAfterNavigate");
|
|
7126
7273
|
}
|
|
7274
|
+
async copyToClipboard(value) {
|
|
7275
|
+
const text = value == null ? "" : typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
7276
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
7277
|
+
await navigator.clipboard.writeText(text);
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
if (typeof document === "undefined") {
|
|
7281
|
+
throw new Error("Clipboard API is unavailable");
|
|
7282
|
+
}
|
|
7283
|
+
const textarea = document.createElement("textarea");
|
|
7284
|
+
textarea.value = text;
|
|
7285
|
+
textarea.setAttribute("readonly", "true");
|
|
7286
|
+
textarea.style.position = "fixed";
|
|
7287
|
+
textarea.style.left = "-9999px";
|
|
7288
|
+
textarea.style.top = "0";
|
|
7289
|
+
document.body.append(textarea);
|
|
7290
|
+
textarea.focus();
|
|
7291
|
+
textarea.select();
|
|
7292
|
+
const copied = document.execCommand("copy");
|
|
7293
|
+
textarea.remove();
|
|
7294
|
+
if (!copied) {
|
|
7295
|
+
throw new Error("Failed to copy value to clipboard");
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
async selectFile(args) {
|
|
7299
|
+
if (typeof document === "undefined") {
|
|
7300
|
+
throw new Error("File selection is unavailable");
|
|
7301
|
+
}
|
|
7302
|
+
const options = isPlainObject4(args) ? args : {};
|
|
7303
|
+
const input = document.createElement("input");
|
|
7304
|
+
input.type = "file";
|
|
7305
|
+
input.style.position = "fixed";
|
|
7306
|
+
input.style.left = "-9999px";
|
|
7307
|
+
input.style.top = "0";
|
|
7308
|
+
if (typeof options.accept === "string" && options.accept.trim()) {
|
|
7309
|
+
input.accept = options.accept;
|
|
7310
|
+
}
|
|
7311
|
+
input.multiple = options.multiple === true;
|
|
7312
|
+
const selection = new Promise((resolve, reject) => {
|
|
7313
|
+
let settled = false;
|
|
7314
|
+
const finish = (value) => {
|
|
7315
|
+
if (settled) {
|
|
7316
|
+
return;
|
|
7317
|
+
}
|
|
7318
|
+
settled = true;
|
|
7319
|
+
resolve(value);
|
|
7320
|
+
};
|
|
7321
|
+
const fail = (error) => {
|
|
7322
|
+
if (settled) {
|
|
7323
|
+
return;
|
|
7324
|
+
}
|
|
7325
|
+
settled = true;
|
|
7326
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
7327
|
+
};
|
|
7328
|
+
const cleanup = () => {
|
|
7329
|
+
window.removeEventListener("focus", handleFocus, true);
|
|
7330
|
+
input.remove();
|
|
7331
|
+
};
|
|
7332
|
+
const handleFocus = () => {
|
|
7333
|
+
window.setTimeout(() => {
|
|
7334
|
+
if (!settled && (!input.files || input.files.length === 0)) {
|
|
7335
|
+
cleanup();
|
|
7336
|
+
finish(null);
|
|
7337
|
+
}
|
|
7338
|
+
}, 250);
|
|
7339
|
+
};
|
|
7340
|
+
input.addEventListener("change", () => {
|
|
7341
|
+
void (async () => {
|
|
7342
|
+
try {
|
|
7343
|
+
const files = Array.from(input.files ?? []);
|
|
7344
|
+
const converted = await Promise.all(files.map((file) => this.readSelectedFile(file)));
|
|
7345
|
+
cleanup();
|
|
7346
|
+
finish(input.multiple ? converted : converted[0] ?? null);
|
|
7347
|
+
} catch (error) {
|
|
7348
|
+
cleanup();
|
|
7349
|
+
fail(error);
|
|
7350
|
+
}
|
|
7351
|
+
})();
|
|
7352
|
+
}, { once: true });
|
|
7353
|
+
input.addEventListener("cancel", () => {
|
|
7354
|
+
cleanup();
|
|
7355
|
+
finish(null);
|
|
7356
|
+
}, { once: true });
|
|
7357
|
+
window.addEventListener("focus", handleFocus, true);
|
|
7358
|
+
document.body.append(input);
|
|
7359
|
+
input.click();
|
|
7360
|
+
});
|
|
7361
|
+
return selection;
|
|
7362
|
+
}
|
|
7363
|
+
readSelectedFile(file) {
|
|
7364
|
+
return new Promise((resolve, reject) => {
|
|
7365
|
+
const reader = new FileReader();
|
|
7366
|
+
reader.onerror = () => {
|
|
7367
|
+
reject(reader.error ?? new Error(`Failed to read file ${file.name}`));
|
|
7368
|
+
};
|
|
7369
|
+
reader.onload = () => {
|
|
7370
|
+
const result = typeof reader.result === "string" ? reader.result : "";
|
|
7371
|
+
const [, data = ""] = result.split(",", 2);
|
|
7372
|
+
resolve({
|
|
7373
|
+
name: file.name,
|
|
7374
|
+
mimeType: file.type || "application/octet-stream",
|
|
7375
|
+
size: file.size,
|
|
7376
|
+
data
|
|
7377
|
+
});
|
|
7378
|
+
};
|
|
7379
|
+
reader.readAsDataURL(file);
|
|
7380
|
+
});
|
|
7381
|
+
}
|
|
7382
|
+
async confirmModal(args, _inputValue) {
|
|
7383
|
+
const options = isPlainObject4(args) ? args : {};
|
|
7384
|
+
const caption = typeof options.caption === "string" && options.caption ? options.caption : "Confirm?";
|
|
7385
|
+
const yes = typeof options.yes === "string" && options.yes ? options.yes : "Yes";
|
|
7386
|
+
const no = typeof options.no === "string" && options.no ? options.no : "No";
|
|
7387
|
+
return new Promise((resolve) => {
|
|
7388
|
+
this.activeConfirmModal?.resolve(false);
|
|
7389
|
+
this.activeConfirmModal = {
|
|
7390
|
+
caption,
|
|
7391
|
+
yes,
|
|
7392
|
+
no,
|
|
7393
|
+
resolve
|
|
7394
|
+
};
|
|
7395
|
+
void this.rerenderRoot();
|
|
7396
|
+
});
|
|
7397
|
+
}
|
|
7398
|
+
resolveConfirmModal(confirmed) {
|
|
7399
|
+
const activeModal = this.activeConfirmModal;
|
|
7400
|
+
if (!activeModal) {
|
|
7401
|
+
return;
|
|
7402
|
+
}
|
|
7403
|
+
this.activeConfirmModal = null;
|
|
7404
|
+
activeModal.resolve(confirmed);
|
|
7405
|
+
}
|
|
7127
7406
|
reindexStaticTree() {
|
|
7128
7407
|
this.nodeByKey.clear();
|
|
7129
7408
|
this.nodeById.clear();
|
|
@@ -7178,7 +7457,11 @@ var RuntimeRenderer = class {
|
|
|
7178
7457
|
stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
|
|
7179
7458
|
vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
|
|
7180
7459
|
activeModalId: this.activeModalId,
|
|
7181
|
-
activeContextMenu: this.activeContextMenu ?
|
|
7460
|
+
activeContextMenu: this.activeContextMenu ? {
|
|
7461
|
+
id: this.activeContextMenu.id,
|
|
7462
|
+
x: this.activeContextMenu.x,
|
|
7463
|
+
y: this.activeContextMenu.y
|
|
7464
|
+
} : null
|
|
7182
7465
|
};
|
|
7183
7466
|
}
|
|
7184
7467
|
indexStaticNodes(node, path) {
|
|
@@ -7688,7 +7971,8 @@ var RuntimeRenderer = class {
|
|
|
7688
7971
|
const env = this.getWidgetRenderEnvironment();
|
|
7689
7972
|
const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
|
|
7690
7973
|
const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
|
|
7691
|
-
|
|
7974
|
+
const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
|
|
7975
|
+
return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
|
|
7692
7976
|
}
|
|
7693
7977
|
updatePointerFromEvent(event) {
|
|
7694
7978
|
this.lastPointer = updatePointerFromMouseEvent(event);
|
|
@@ -7915,6 +8199,7 @@ var RuntimeRenderer = class {
|
|
|
7915
8199
|
nodeByKey: this.nodeByKey,
|
|
7916
8200
|
stateByKey: this.stateByKey,
|
|
7917
8201
|
getLastPointer: () => this.lastPointer,
|
|
8202
|
+
getActiveContextMenu: () => this.activeContextMenu,
|
|
7918
8203
|
setActiveContextMenu: (menu) => {
|
|
7919
8204
|
this.activeContextMenu = menu;
|
|
7920
8205
|
},
|
|
@@ -7924,6 +8209,7 @@ var RuntimeRenderer = class {
|
|
|
7924
8209
|
updateSplitterDrag: (clientPosition) => this.updateSplitterDrag(clientPosition),
|
|
7925
8210
|
stopSplitterDrag: () => this.stopSplitterDrag(),
|
|
7926
8211
|
hasSplitterDrag: () => this.splitterDragState !== null,
|
|
8212
|
+
resolveConfirmModal: (confirmed) => this.resolveConfirmModal(confirmed),
|
|
7927
8213
|
closeModal: (modalId) => this.closeModal(modalId),
|
|
7928
8214
|
rerenderRoot: () => this.rerenderRoot(),
|
|
7929
8215
|
redispatchUnderlyingClick: (x, y) => this.redispatchUnderlyingClick(x, y),
|
|
@@ -34,6 +34,9 @@ type ActionRuntimeOptions = {
|
|
|
34
34
|
focusWidget: (reference: string) => void;
|
|
35
35
|
playAudio: (value: unknown) => Promise<void>;
|
|
36
36
|
stopPlaying: () => Promise<void>;
|
|
37
|
+
copyToClipboard: (value: unknown) => Promise<void>;
|
|
38
|
+
selectFile: (options: unknown) => Promise<unknown>;
|
|
39
|
+
confirmModal: (options: unknown, inputValue: unknown) => Promise<boolean>;
|
|
37
40
|
startRecording: () => Promise<void>;
|
|
38
41
|
stopRecording: () => Promise<void>;
|
|
39
42
|
startListening: () => Promise<void>;
|
|
@@ -53,6 +56,8 @@ type ActionRuntimeOptions = {
|
|
|
53
56
|
id: string;
|
|
54
57
|
x: number;
|
|
55
58
|
y: number;
|
|
59
|
+
currentValue?: unknown;
|
|
60
|
+
openPath?: number[];
|
|
56
61
|
} | null) => void;
|
|
57
62
|
setActiveModalId: (modalId: string | null) => void;
|
|
58
63
|
closeModal: (modalId?: string | null) => void;
|
|
@@ -79,6 +84,9 @@ export declare class ActionRuntime {
|
|
|
79
84
|
private readonly focusWidget;
|
|
80
85
|
private readonly playAudio;
|
|
81
86
|
private readonly stopPlaying;
|
|
87
|
+
private readonly copyToClipboard;
|
|
88
|
+
private readonly selectFile;
|
|
89
|
+
private readonly confirmModal;
|
|
82
90
|
private readonly startRecording;
|
|
83
91
|
private readonly stopRecording;
|
|
84
92
|
private readonly startListening;
|
|
@@ -106,5 +114,6 @@ export declare class ActionRuntime {
|
|
|
106
114
|
private getCurrentListState;
|
|
107
115
|
private cloneValue;
|
|
108
116
|
private stringifyValue;
|
|
117
|
+
private isCurrentScopedReference;
|
|
109
118
|
}
|
|
110
119
|
export {};
|
package/dist/lib/dom-state.d.ts
CHANGED
|
@@ -21,10 +21,14 @@ export declare function adjustContextMenuPosition(root: HTMLElement, activeConte
|
|
|
21
21
|
id: string;
|
|
22
22
|
x: number;
|
|
23
23
|
y: number;
|
|
24
|
+
currentValue?: unknown;
|
|
25
|
+
openPath?: number[];
|
|
24
26
|
} | null): {
|
|
25
27
|
id: string;
|
|
26
28
|
x: number;
|
|
27
29
|
y: number;
|
|
30
|
+
currentValue?: unknown;
|
|
31
|
+
openPath?: number[];
|
|
28
32
|
} | null;
|
|
29
33
|
export declare function updatePointerFromMouseEvent(event: MouseEvent): {
|
|
30
34
|
x: number;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { ActionDefinition, BaseNode } from '../types.js';
|
|
2
2
|
import type { WidgetRenderEnvironment } from './context.js';
|
|
3
|
+
export type ContextMenuItem = {
|
|
4
|
+
name: string;
|
|
5
|
+
actions?: ActionDefinition[];
|
|
6
|
+
items?: ContextMenuItem[];
|
|
7
|
+
};
|
|
3
8
|
export type ContextMenuNode = BaseNode & {
|
|
4
9
|
widget: 'context-menu';
|
|
5
|
-
items?:
|
|
6
|
-
name: string;
|
|
7
|
-
actions?: ActionDefinition[];
|
|
8
|
-
}>;
|
|
10
|
+
items?: ContextMenuItem[];
|
|
9
11
|
};
|
|
10
|
-
export
|
|
12
|
+
export type ActiveContextMenuState = {
|
|
11
13
|
id: string;
|
|
12
14
|
x: number;
|
|
13
15
|
y: number;
|
|
14
|
-
|
|
16
|
+
currentValue?: unknown;
|
|
17
|
+
openPath?: number[];
|
|
18
|
+
};
|
|
19
|
+
export declare function getContextMenuItemAtPath(items: ContextMenuItem[] | undefined, path: number[]): ContextMenuItem | null;
|
|
20
|
+
export declare function renderContextMenuOverlay(env: WidgetRenderEnvironment, menu: ActiveContextMenuState): string;
|
|
@@ -5,7 +5,7 @@ import type { CheckboxNode } from './checkbox.js';
|
|
|
5
5
|
import type { ComboboxNode } from './combobox.js';
|
|
6
6
|
import type { ConditionalContainerNode } from './conditional-container.js';
|
|
7
7
|
import type { ContainerCell, ContainerLayoutNode } from './container-layout.js';
|
|
8
|
-
import type { ContextMenuNode } from './context-menu.js';
|
|
8
|
+
import type { ActiveContextMenuState, ContextMenuNode } from './context-menu.js';
|
|
9
9
|
import type { GridViewNode } from './grid-view.js';
|
|
10
10
|
import type { ImageNode } from './image.js';
|
|
11
11
|
import type { LinkNode } from './link.js';
|
|
@@ -23,11 +23,7 @@ import type { TabsNode } from './tabs.js';
|
|
|
23
23
|
import type { TextareaNode } from './textarea.js';
|
|
24
24
|
import type { UiReferenceNode } from './ui-reference.js';
|
|
25
25
|
export type WidgetRenderEnvironment = {
|
|
26
|
-
readonly activeContextMenu:
|
|
27
|
-
id: string;
|
|
28
|
-
x: number;
|
|
29
|
-
y: number;
|
|
30
|
-
} | null;
|
|
26
|
+
readonly activeContextMenu: ActiveContextMenuState | null;
|
|
31
27
|
readonly activeModalId: string | null;
|
|
32
28
|
readonly nodeById: Map<string, DescriptionNode>;
|
|
33
29
|
escapeHtml: (value: unknown) => string;
|
|
@@ -7,10 +7,19 @@ type DelegationEnvironment = {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
};
|
|
10
|
+
getActiveContextMenu: () => {
|
|
11
|
+
id: string;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
currentValue?: unknown;
|
|
15
|
+
openPath?: number[];
|
|
16
|
+
} | null;
|
|
10
17
|
setActiveContextMenu: (menu: {
|
|
11
18
|
id: string;
|
|
12
19
|
x: number;
|
|
13
20
|
y: number;
|
|
21
|
+
currentValue?: unknown;
|
|
22
|
+
openPath?: number[];
|
|
14
23
|
} | null) => void;
|
|
15
24
|
isWidgetEnabled: (node: DescriptionNode, key: string) => boolean;
|
|
16
25
|
startSplitterDrag: (layoutKey: string, axis: 'horizontal' | 'vertical', splitterIndex: number, startPointer: number) => void;
|
|
@@ -18,6 +27,7 @@ type DelegationEnvironment = {
|
|
|
18
27
|
updateSplitterDrag: (clientPosition: number) => void;
|
|
19
28
|
stopSplitterDrag: () => void;
|
|
20
29
|
hasSplitterDrag: () => boolean;
|
|
30
|
+
resolveConfirmModal: (confirmed: boolean) => void;
|
|
21
31
|
closeModal: (modalId: string | null) => void;
|
|
22
32
|
rerenderRoot: () => Promise<void>;
|
|
23
33
|
redispatchUnderlyingClick: (clientX: number, clientY: number) => void;
|
package/package.json
CHANGED
package/vjt-styles.css
CHANGED
|
@@ -685,12 +685,21 @@ body {
|
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
.vjt-modal-backdrop,
|
|
688
|
-
.vjt-context-menu-backdrop
|
|
688
|
+
.vjt-context-menu-backdrop,
|
|
689
|
+
.vjt-confirm-backdrop {
|
|
689
690
|
position: fixed;
|
|
690
691
|
inset: 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.vjt-modal-backdrop,
|
|
695
|
+
.vjt-context-menu-backdrop {
|
|
691
696
|
z-index: 1000;
|
|
692
697
|
}
|
|
693
698
|
|
|
699
|
+
.vjt-confirm-backdrop {
|
|
700
|
+
z-index: 1100;
|
|
701
|
+
}
|
|
702
|
+
|
|
694
703
|
.vjt-modal-backdrop {
|
|
695
704
|
display: flex;
|
|
696
705
|
align-items: center;
|
|
@@ -759,16 +768,20 @@ body {
|
|
|
759
768
|
}
|
|
760
769
|
|
|
761
770
|
.vjt-context-menu {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
771
|
+
position: fixed;
|
|
772
|
+
box-sizing: border-box;
|
|
773
|
+
width: 220px;
|
|
774
|
+
padding: 8px;
|
|
775
|
+
border-radius: 14px;
|
|
766
776
|
border: 1px solid var(--vjt-border);
|
|
767
777
|
background: var(--vjt-surface);
|
|
768
778
|
box-shadow: var(--vjt-shadow);
|
|
769
779
|
}
|
|
770
780
|
|
|
771
781
|
.vjt-context-menu-item {
|
|
782
|
+
display: flex;
|
|
783
|
+
align-items: center;
|
|
784
|
+
justify-content: space-between;
|
|
772
785
|
width: 100%;
|
|
773
786
|
padding: 10px 12px;
|
|
774
787
|
text-align: left;
|
|
@@ -781,3 +794,48 @@ body {
|
|
|
781
794
|
.vjt-context-menu-item:hover {
|
|
782
795
|
background: color-mix(in srgb, var(--vjt-accent) 14%, transparent);
|
|
783
796
|
}
|
|
797
|
+
|
|
798
|
+
.vjt-context-menu-item--parent {
|
|
799
|
+
font-weight: 500;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.vjt-context-menu-chevron {
|
|
803
|
+
margin-left: 16px;
|
|
804
|
+
opacity: 0.7;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.vjt-confirm-backdrop {
|
|
808
|
+
display: flex;
|
|
809
|
+
align-items: center;
|
|
810
|
+
justify-content: center;
|
|
811
|
+
background: rgba(7, 10, 15, 0.42);
|
|
812
|
+
padding: 24px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.vjt-confirm-window {
|
|
816
|
+
width: min(420px, calc(100vw - 32px));
|
|
817
|
+
display: flex;
|
|
818
|
+
flex-direction: column;
|
|
819
|
+
gap: 18px;
|
|
820
|
+
padding: 22px;
|
|
821
|
+
border-radius: 18px;
|
|
822
|
+
border: 1px solid var(--vjt-border);
|
|
823
|
+
background: var(--vjt-modal-surface);
|
|
824
|
+
color: var(--vjt-text);
|
|
825
|
+
box-shadow: var(--vjt-shadow);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.vjt-confirm-caption {
|
|
829
|
+
line-height: 1.45;
|
|
830
|
+
white-space: pre-wrap;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.vjt-confirm-actions {
|
|
834
|
+
display: flex;
|
|
835
|
+
justify-content: flex-end;
|
|
836
|
+
gap: 10px;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.vjt-confirm-actions .vjt-button {
|
|
840
|
+
min-width: 120px;
|
|
841
|
+
}
|