pgkiln 0.42.0 → 0.43.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/db/migrations/099_layout_positions.sql +29 -0
- package/docs/apex-feature-parity.md +34 -7
- package/docs/guide/03-builder.md +67 -13
- package/docs/guide/04-pages-and-regions.md +46 -3
- package/docs/guide/05-items.md +1 -1
- package/docs/guide/09-reference.md +4 -1
- package/docs/guide/12-development.md +2 -1
- package/package.json +1 -1
- package/public/app.css +58 -0
- package/public/app.js +183 -0
- package/public/builder.css +112 -0
- package/public/builder.js +280 -3
- package/src/builder/arrange.ts +133 -17
- package/src/builder/components.ts +28 -22
- package/src/builder/designer.ts +67 -18
- package/src/builder/forms.ts +4 -1
- package/src/builder/object-browser.ts +342 -0
- package/src/builder/pwa.ts +3 -3
- package/src/builder/routes.ts +85 -26
- package/src/builder/shared.ts +103 -1
- package/src/builder/sql.ts +13 -65
- package/src/builder/ui.ts +21 -6
- package/src/i18n/ar.ts +16 -0
- package/src/i18n/cs.ts +16 -0
- package/src/i18n/da.ts +16 -0
- package/src/i18n/de.ts +16 -0
- package/src/i18n/el.ts +16 -0
- package/src/i18n/es.ts +16 -0
- package/src/i18n/fi.ts +16 -0
- package/src/i18n/fr.ts +16 -0
- package/src/i18n/he.ts +16 -0
- package/src/i18n/it.ts +16 -0
- package/src/i18n/ja.ts +16 -0
- package/src/i18n/ko.ts +16 -0
- package/src/i18n/nb.ts +16 -0
- package/src/i18n/pl.ts +16 -0
- package/src/i18n/pt.ts +16 -0
- package/src/i18n/ru.ts +16 -0
- package/src/i18n/sv.ts +16 -0
- package/src/i18n/tr.ts +16 -0
- package/src/i18n/uk.ts +16 -0
- package/src/i18n/zh.ts +16 -0
- package/src/i18n.ts +32 -0
- package/src/metadata.ts +5 -1
- package/src/runtime/engine.ts +6 -0
- package/src/runtime/format.ts +52 -0
- package/src/runtime/items.ts +27 -0
- package/src/runtime/locale.ts +3 -0
- package/src/runtime/regions.ts +36 -2
- package/src/runtime/render.ts +20 -4
- package/src/runtime/report.ts +134 -79
- package/src/runtime/routes.ts +34 -3
package/public/app.js
CHANGED
|
@@ -3725,3 +3725,186 @@ function areaFilter(map, f) {
|
|
|
3725
3725
|
}, true);
|
|
3726
3726
|
document.addEventListener('keydown', (e) => { if (on && e.key === 'Escape') set(false); });
|
|
3727
3727
|
})();
|
|
3728
|
+
|
|
3729
|
+
// (sprint 76) the page's texts for the report menus below (render.ts CLIENT_TEXTS)
|
|
3730
|
+
const reportTexts = (() => {
|
|
3731
|
+
try {
|
|
3732
|
+
return JSON.parse(document.getElementById('pgkiln-meta')?.textContent || '{}').texts || {};
|
|
3733
|
+
} catch {
|
|
3734
|
+
return {};
|
|
3735
|
+
}
|
|
3736
|
+
})();
|
|
3737
|
+
|
|
3738
|
+
// (sprint 76) The interactive report's Actions menu: an entry opens its form in a dialog (APEX: the
|
|
3739
|
+
// Actions dialogs); the form fields stay the same elements, moved into the dialog and back on close.
|
|
3740
|
+
(() => {
|
|
3741
|
+
const close = (details) => { if (details) details.open = false; };
|
|
3742
|
+
document.addEventListener('toggle', (e) => {
|
|
3743
|
+
const item = e.target;
|
|
3744
|
+
if (!(item instanceof HTMLDetailsElement) || !item.matches('.ir-item[data-ir-dialog]') || !item.open) return;
|
|
3745
|
+
const body = item.querySelector(':scope > .ir-body');
|
|
3746
|
+
const menu = item.closest('details.actions-menu');
|
|
3747
|
+
if (!body || typeof HTMLDialogElement !== 'function') return;
|
|
3748
|
+
item.open = false;
|
|
3749
|
+
menu?.querySelectorAll('details.ir-sub[open]').forEach(close);
|
|
3750
|
+
close(menu);
|
|
3751
|
+
const dialog = document.createElement('dialog');
|
|
3752
|
+
dialog.className = 'ir-dialog';
|
|
3753
|
+
const title = document.createElement('h2');
|
|
3754
|
+
title.textContent = item.querySelector('summary span')?.textContent ?? '';
|
|
3755
|
+
const x = document.createElement('button');
|
|
3756
|
+
x.type = 'button';
|
|
3757
|
+
x.className = 'icon-button ir-dialog-close';
|
|
3758
|
+
x.setAttribute('aria-label', reportTexts['common.dismiss'] || 'Close');
|
|
3759
|
+
x.textContent = '×';
|
|
3760
|
+
x.addEventListener('click', () => dialog.close());
|
|
3761
|
+
const head = document.createElement('div');
|
|
3762
|
+
head.className = 'ir-dialog-head';
|
|
3763
|
+
head.append(title, x);
|
|
3764
|
+
dialog.append(head, body);
|
|
3765
|
+
document.body.append(dialog);
|
|
3766
|
+
dialog.addEventListener('close', () => {
|
|
3767
|
+
item.append(body);
|
|
3768
|
+
dialog.remove();
|
|
3769
|
+
menu?.querySelector('summary')?.focus();
|
|
3770
|
+
});
|
|
3771
|
+
dialog.showModal();
|
|
3772
|
+
body.querySelector('select, input, a, button')?.focus();
|
|
3773
|
+
}, true);
|
|
3774
|
+
// one submenu open at a time
|
|
3775
|
+
document.addEventListener('toggle', (e) => {
|
|
3776
|
+
const sub = e.target;
|
|
3777
|
+
if (!(sub instanceof HTMLDetailsElement) || !sub.matches('.ir-sub') || !sub.open) return;
|
|
3778
|
+
sub.parentElement?.parentElement?.querySelectorAll(':scope > li > details.ir-sub[open]').forEach((d) => { if (d !== sub) d.open = false; });
|
|
3779
|
+
}, true);
|
|
3780
|
+
})();
|
|
3781
|
+
|
|
3782
|
+
// (sprint 76) A report column heading opens a menu: sort ascending or descending, hide, control break, and
|
|
3783
|
+
// the column's values (with counts, a search box) to filter by; the sort arrow still sorts at once.
|
|
3784
|
+
(() => {
|
|
3785
|
+
let open = null;
|
|
3786
|
+
const shut = () => { open?.remove(); open = null; };
|
|
3787
|
+
document.addEventListener('click', (e) => {
|
|
3788
|
+
const a = e.target.closest?.('th[data-col] > a');
|
|
3789
|
+
if (!a) {
|
|
3790
|
+
if (open && !open.contains(e.target)) shut();
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
const th = a.parentElement;
|
|
3794
|
+
// the sort indicator keeps sorting
|
|
3795
|
+
if (e.target.closest('.sort-ind') && th.getAttribute('aria-sort') !== 'none') return;
|
|
3796
|
+
e.preventDefault();
|
|
3797
|
+
shut();
|
|
3798
|
+
const menu = document.createElement('div');
|
|
3799
|
+
menu.className = 'th-menu menu-panel';
|
|
3800
|
+
menu.setAttribute('role', 'dialog');
|
|
3801
|
+
menu.setAttribute('aria-label', th.dataset.label);
|
|
3802
|
+
const t = (k, d) => reportTexts[k] || d;
|
|
3803
|
+
const link = (href, text) => { const l = document.createElement('a'); l.href = href; l.textContent = text; return l; };
|
|
3804
|
+
const actions = document.createElement('div');
|
|
3805
|
+
actions.className = 'th-menu-actions';
|
|
3806
|
+
actions.append(link(th.dataset.asc, `▲ ${t('report.sort_asc', 'Sort ascending')}`), link(th.dataset.desc, `▼ ${t('report.sort_desc', 'Sort descending')}`),
|
|
3807
|
+
link(th.dataset.hide, t('report.hide', 'Hide column')), link(th.dataset.break, t('report.break', 'Control break')));
|
|
3808
|
+
const search = document.createElement('input');
|
|
3809
|
+
search.type = 'search';
|
|
3810
|
+
search.placeholder = t('report.filter_values', 'Filter the values');
|
|
3811
|
+
search.setAttribute('aria-label', search.placeholder);
|
|
3812
|
+
const list = document.createElement('ul');
|
|
3813
|
+
list.className = 'th-menu-values';
|
|
3814
|
+
menu.append(actions, search, list);
|
|
3815
|
+
document.body.append(menu);
|
|
3816
|
+
const box = th.getBoundingClientRect();
|
|
3817
|
+
menu.style.setProperty('position', 'absolute');
|
|
3818
|
+
menu.style.setProperty('top', `${window.scrollY + box.bottom + 4}px`);
|
|
3819
|
+
menu.style.setProperty('left', `${Math.max(8, Math.min(window.scrollX + box.left, window.scrollX + document.documentElement.clientWidth - 288))}px`);
|
|
3820
|
+
open = menu;
|
|
3821
|
+
const here = new URL(location.href);
|
|
3822
|
+
const url = new URL(th.dataset.values, location.href);
|
|
3823
|
+
here.searchParams.forEach((v, k) => url.searchParams.append(k, v));
|
|
3824
|
+
url.searchParams.set('col', th.dataset.col);
|
|
3825
|
+
fetch(url, { headers: { accept: 'application/json' }, credentials: 'same-origin' })
|
|
3826
|
+
.then((r) => (r.ok ? r.json() : { values: [] }))
|
|
3827
|
+
.then(({ values }) => {
|
|
3828
|
+
if (!values?.length) {
|
|
3829
|
+
const li = document.createElement('li');
|
|
3830
|
+
li.className = 'muted';
|
|
3831
|
+
li.textContent = t('report.no_values', 'No values');
|
|
3832
|
+
list.append(li);
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
for (const { value, count } of values) {
|
|
3836
|
+
if (value === null) continue;
|
|
3837
|
+
const target = new URL(location.href);
|
|
3838
|
+
for (const k of [...target.searchParams.keys()]) if (/^r\d+_(p|k)$/.test(k)) target.searchParams.delete(k);
|
|
3839
|
+
target.searchParams.append(th.dataset.filter, `${th.dataset.col}|eq|${value}`);
|
|
3840
|
+
const li = document.createElement('li');
|
|
3841
|
+
const l = link(target.toString(), value);
|
|
3842
|
+
const n = document.createElement('span');
|
|
3843
|
+
n.className = 'muted';
|
|
3844
|
+
n.textContent = String(count);
|
|
3845
|
+
l.append(n);
|
|
3846
|
+
li.append(l);
|
|
3847
|
+
list.append(li);
|
|
3848
|
+
}
|
|
3849
|
+
})
|
|
3850
|
+
.catch(() => {});
|
|
3851
|
+
search.addEventListener('input', () => {
|
|
3852
|
+
const q = search.value.trim().toLowerCase();
|
|
3853
|
+
list.querySelectorAll('li').forEach((li) => { li.hidden = !!q && !li.textContent.toLowerCase().includes(q); });
|
|
3854
|
+
});
|
|
3855
|
+
actions.querySelector('a')?.focus();
|
|
3856
|
+
menu.addEventListener('keydown', (k) => { if (k.key === 'Escape') { shut(); a.focus(); } });
|
|
3857
|
+
});
|
|
3858
|
+
window.addEventListener('resize', shut);
|
|
3859
|
+
})();
|
|
3860
|
+
|
|
3861
|
+
// (sprint 76) the report pager's rows per page: sent on change
|
|
3862
|
+
document.addEventListener('change', (e) => {
|
|
3863
|
+
if (e.target.matches?.('select[data-go]')) e.target.form?.submit();
|
|
3864
|
+
});
|
|
3865
|
+
|
|
3866
|
+
// (sprint 76) A date item in the application's date format: the calendar button opens the browser's date
|
|
3867
|
+
// picker on a hidden date field; the chosen date is written into the text field in the mask.
|
|
3868
|
+
(() => {
|
|
3869
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
3870
|
+
const TOKEN = /"[^"]*"|YYYY|YY|MONTH|MON|MM|DD/gi;
|
|
3871
|
+
const format = (iso, mask, months) => {
|
|
3872
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
3873
|
+
if (!m) return '';
|
|
3874
|
+
const [y, mo, d] = m.slice(1).map(Number);
|
|
3875
|
+
return mask.replace(TOKEN, (tok) => {
|
|
3876
|
+
if (tok.startsWith('"')) return tok.slice(1, -1);
|
|
3877
|
+
const up = tok.toUpperCase();
|
|
3878
|
+
const cased = (s) => (tok === up ? s.toUpperCase() : tok[1] && tok[1] !== up[1] ? s[0].toUpperCase() + s.slice(1) : s);
|
|
3879
|
+
if (up === 'YYYY') return String(y);
|
|
3880
|
+
if (up === 'YY') return pad(y % 100);
|
|
3881
|
+
if (up === 'MM') return pad(mo);
|
|
3882
|
+
if (up === 'DD') return pad(d);
|
|
3883
|
+
if (up === 'MONTH') return cased(months.long[mo - 1]);
|
|
3884
|
+
if (up === 'MON') return cased(months.short[mo - 1]);
|
|
3885
|
+
return tok;
|
|
3886
|
+
});
|
|
3887
|
+
};
|
|
3888
|
+
document.addEventListener('click', (e) => {
|
|
3889
|
+
const btn = e.target.closest?.('[data-date-pick]');
|
|
3890
|
+
if (!btn) return;
|
|
3891
|
+
const native = document.querySelector(`input[data-date-for="${CSS.escape(btn.dataset.datePick)}"]`);
|
|
3892
|
+
if (!native) return;
|
|
3893
|
+
try {
|
|
3894
|
+
native.showPicker();
|
|
3895
|
+
} catch {
|
|
3896
|
+
native.focus();
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
document.addEventListener('change', (e) => {
|
|
3900
|
+
const native = e.target;
|
|
3901
|
+
if (!native.matches?.('input[data-date-for]')) return;
|
|
3902
|
+
const text = document.getElementById(native.dataset.dateFor);
|
|
3903
|
+
if (!text) return;
|
|
3904
|
+
let months = { long: [], short: [] };
|
|
3905
|
+
try { months = JSON.parse(text.dataset.months || '{}'); } catch {}
|
|
3906
|
+
text.value = format(native.value, text.dataset.dateMask || 'YYYY-MM-DD', months);
|
|
3907
|
+
text.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3908
|
+
text.focus();
|
|
3909
|
+
});
|
|
3910
|
+
})();
|
package/public/builder.css
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
--bg: #1d1c1b; --surface: #2a2927; --surface-2: #31302d; --surface-3: #3b3936; --text: #e9e6e1; --muted: #aaa59d;
|
|
45
45
|
--border: #44423e; --accent: #62b2ea; --accent-text: #0b1824; --accent-soft: #233a4c; --gold: #e3b75e;
|
|
46
46
|
--canvas: #211f1e; --block: #33322f; --block-head: #3d3b38; --code-bg: #242321; --warn: #e0a740;
|
|
47
|
+
--pd-c-region: #7fb4ea; --pd-c-item: #7ccf8c; --pd-c-button: #f0a35e; --pd-c-da: #c49cf0; --pd-c-process: #ee8fbf;
|
|
47
48
|
color-scheme: dark;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
--bg: #1d1c1b; --surface: #2a2927; --surface-2: #31302d; --surface-3: #3b3936; --text: #e9e6e1; --muted: #aaa59d;
|
|
52
53
|
--border: #44423e; --accent: #62b2ea; --accent-text: #0b1824; --accent-soft: #233a4c; --gold: #e3b75e;
|
|
53
54
|
--canvas: #211f1e; --block: #33322f; --block-head: #3d3b38; --code-bg: #242321; --warn: #e0a740;
|
|
55
|
+
--pd-c-region: #7fb4ea; --pd-c-item: #7ccf8c; --pd-c-button: #f0a35e; --pd-c-da: #c49cf0; --pd-c-process: #ee8fbf;
|
|
54
56
|
color-scheme: dark;
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -900,3 +902,113 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
|
|
|
900
902
|
.progress-spinner { width: 1.25rem; height: 1.25rem; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--accent); animation: progress-spin 0.8s linear infinite; }
|
|
901
903
|
@keyframes progress-spin { to { transform: rotate(360deg); } }
|
|
902
904
|
@media (prefers-reduced-motion: reduce) { .progress-spinner { animation: none; } }
|
|
905
|
+
|
|
906
|
+
/* (sprint 74) the default-password warning: dismissible; a dot on the avatar */
|
|
907
|
+
.alert-dismissible { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
|
|
908
|
+
.alert-dismissible form { margin: 0; }
|
|
909
|
+
.rail-avatar.has-dot { position: relative; }
|
|
910
|
+
.rail-avatar.has-dot::after { content: ""; position: absolute; top: 2px; right: 2px; width: 9px; height: 9px; border-radius: 50%; background: var(--danger, #c23028); box-shadow: 0 0 0 2px var(--rail-bg, #1b2330); }
|
|
911
|
+
.weak-note { color: var(--danger, #c23028) !important; }
|
|
912
|
+
|
|
913
|
+
/* (sprint 74) Shared Components: groups of component types with counts; a list per type */
|
|
914
|
+
.sc-head, .sc-list-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
915
|
+
.sc-list-head h1 { margin: 0; }
|
|
916
|
+
.sc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: 1rem; align-items: start; }
|
|
917
|
+
.sc-group .region-header h2 { display: flex; align-items: center; gap: 0.5rem; }
|
|
918
|
+
.sc-links { list-style: none; margin: 0; padding: 0.4rem 0; }
|
|
919
|
+
.sc-links li { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.3rem 1rem; }
|
|
920
|
+
.sc-links a { overflow-wrap: anywhere; min-width: 0; }
|
|
921
|
+
.sc-count { font-size: 0.78rem; font-weight: 600; color: var(--muted); background: var(--surface-3); border-radius: 99px; padding: 0.05rem 0.5rem; }
|
|
922
|
+
|
|
923
|
+
/* (sprint 74) application Settings in tabs */
|
|
924
|
+
.settings-tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; border-bottom: 1px solid var(--border); margin: 0 0 1rem; }
|
|
925
|
+
.settings-tabs a { padding: 0.5rem 0.85rem; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
926
|
+
.settings-tabs a:hover { color: var(--text); }
|
|
927
|
+
.settings-tabs a[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
|
|
928
|
+
.settings-body { display: grid; gap: 1rem; max-width: 64rem; }
|
|
929
|
+
.settings-form { display: grid; gap: 1rem; }
|
|
930
|
+
|
|
931
|
+
/* (sprint 74) the application home: search, view and Create page above the pages; Tasks on the right */
|
|
932
|
+
.app-home { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; gap: 1rem; align-items: start; }
|
|
933
|
+
@media (max-width: 1100px) { .app-home { grid-template-columns: 1fr; } }
|
|
934
|
+
.app-home-main { display: grid; gap: 1rem; min-width: 0; }
|
|
935
|
+
.pages-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
|
936
|
+
.pages-bar .search { flex: 1 1 16rem; margin: 0; }
|
|
937
|
+
.pages-bar .search input { flex: 1; min-width: 0; }
|
|
938
|
+
.view-toggle a { display: inline-flex; align-items: center; gap: 0.3rem; }
|
|
939
|
+
.page-icons { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.6rem; }
|
|
940
|
+
.page-icon { display: grid; gap: 0.15rem; padding: 0.7rem 0.8rem; border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
|
|
941
|
+
.page-icon:hover, .page-icon:focus-visible { border-color: var(--accent); }
|
|
942
|
+
.page-icon-no { font-size: 1.3rem; font-weight: 700; color: var(--accent); }
|
|
943
|
+
.app-tasks .sc-links a { display: inline-flex; align-items: center; gap: 0.45rem; }
|
|
944
|
+
|
|
945
|
+
/* (sprint 75) SQL Workshop: main tools as tabs and a More menu (no horizontal scrolling); the Object Browser */
|
|
946
|
+
.sw-tabs { overflow: visible; flex-wrap: wrap; }
|
|
947
|
+
.sw-more { position: relative; }
|
|
948
|
+
.sw-more > summary { list-style: none; cursor: pointer; }
|
|
949
|
+
.sw-more > summary::-webkit-details-marker { display: none; }
|
|
950
|
+
.sw-more .menu-panel a[aria-current] { font-weight: 600; }
|
|
951
|
+
.ob-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 0.75rem; margin-bottom: 1rem; }
|
|
952
|
+
.ob-schema { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
953
|
+
.ob-schema .label { margin: 0; }
|
|
954
|
+
.ob-create > summary { list-style: none; }
|
|
955
|
+
.ob-create > summary::-webkit-details-marker { display: none; }
|
|
956
|
+
.ob-filter { padding: 0.5rem; }
|
|
957
|
+
.ob-filter input { width: 100%; }
|
|
958
|
+
.ob-folder > summary { display: flex; align-items: center; gap: 0.45rem; padding: 0.4rem 0.6rem; cursor: pointer; font-weight: 600; list-style: none; }
|
|
959
|
+
.ob-folder > summary::-webkit-details-marker { display: none; }
|
|
960
|
+
.ob-folder > summary .kind { margin-left: auto; }
|
|
961
|
+
.ob-folder .tree { padding-left: 0.75rem; }
|
|
962
|
+
.ob-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
963
|
+
.ob-title { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.5rem; overflow-wrap: anywhere; }
|
|
964
|
+
.ob-actions { display: grid; gap: 0.6rem; margin-top: 1rem; }
|
|
965
|
+
.ob-action { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
966
|
+
.ob-action input:not([type=checkbox]) { width: 10rem; }
|
|
967
|
+
|
|
968
|
+
/* (sprint 77) the page template's display positions and a region's button positions on the layout */
|
|
969
|
+
.pd-zone { display: grid; gap: 0.3em; margin-bottom: 10px; }
|
|
970
|
+
.pd-zone-label { color: var(--muted); font-size: 0.74em; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
971
|
+
.pd-zone-label small { text-transform: none; letter-spacing: 0; font-weight: 400; }
|
|
972
|
+
.pd-zone.is-empty .pd-grid-end { padding: 0.3em; }
|
|
973
|
+
.pd-columns { display: grid; gap: 10px; align-items: start; }
|
|
974
|
+
.pd-columns > .pd-zone { margin-bottom: 0; min-width: 0; }
|
|
975
|
+
.pd-columns:has(> .pd-zone-left) { grid-template-columns: minmax(9rem, 1fr) 3fr; }
|
|
976
|
+
.pd-columns:has(> .pd-zone-right) { grid-template-columns: 3fr minmax(9rem, 1fr); }
|
|
977
|
+
.pd-columns:has(> .pd-zone-left):has(> .pd-zone-right) { grid-template-columns: minmax(9rem, 1fr) 3fr minmax(9rem, 1fr); }
|
|
978
|
+
.pd-columns + .pd-zone { margin-top: 10px; }
|
|
979
|
+
.pd-zone-left .pd-grid > .pd-region, .pd-zone-right .pd-grid > .pd-region { grid-column: 1 / -1; }
|
|
980
|
+
.pd-slots { display: flex; flex-wrap: wrap; gap: 0.3em; }
|
|
981
|
+
.pd-slots-bottom > [data-slot="next"] { margin-left: auto; }
|
|
982
|
+
.pd-slot-spare { display: none; min-height: 1.6em; }
|
|
983
|
+
.pd-dragging-button .pd-slot-spare { display: flex; }
|
|
984
|
+
.pd-canvas:not(.pd-dragging-button) .pd-slots:not(:has(> .pd-slot:not(.pd-slot-spare))) { display: none; }
|
|
985
|
+
@media (max-width: 640px) { .pd-canvas .pd-columns:has(> .pd-zone) { grid-template-columns: minmax(0, 1fr); } }
|
|
986
|
+
|
|
987
|
+
/* (sprint 77) the rendering tree's context menu, the Copy to other page dialog, component colours */
|
|
988
|
+
.pd-menu { position: fixed; z-index: 60; min-width: 13rem; padding: 4px; background: var(--surface, #fff); color: var(--text); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 6px 20px rgb(0 0 0 / 0.18); font-size: 13px; }
|
|
989
|
+
.pd-menu [role=menuitem] { display: block; width: 100%; padding: 0.35rem 0.6rem; border: 0; border-radius: 3px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
990
|
+
.pd-menu [role=menuitem]:is(:hover, :focus-visible) { background: var(--sel-bg); color: var(--sel-text); outline: none; }
|
|
991
|
+
.pd-menu [role=menuitem].is-danger { color: var(--danger); }
|
|
992
|
+
.pd-menu [role=separator] { height: 1px; margin: 4px 2px; background: var(--border); }
|
|
993
|
+
.pd-dialog { width: min(28rem, calc(100vw - 32px)); padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface, #fff); color: var(--text); }
|
|
994
|
+
.pd-dialog::backdrop { background: rgb(0 0 0 / 0.35); }
|
|
995
|
+
.pd-dialog h2 { margin: 0 0 0.5rem; font-size: 1.05rem; }
|
|
996
|
+
.pd-dialog-buttons { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem; }
|
|
997
|
+
.pd-node[data-kind="region"] .icon { color: var(--pd-c-region, #2f6fb3); }
|
|
998
|
+
.pd-node[data-kind="item"] .icon { color: var(--pd-c-item, #2e8540); }
|
|
999
|
+
.pd-node[data-kind="button"] .icon { color: var(--pd-c-button, #b4570f); }
|
|
1000
|
+
.pd-node[data-kind="dynamic_action"] .icon { color: var(--pd-c-da, #8a4fbf); }
|
|
1001
|
+
.pd-node[data-kind="process"] .icon, .pd-node[data-kind="computation"] .icon { color: var(--pd-c-process, #a23b72); }
|
|
1002
|
+
|
|
1003
|
+
/* (sprint 77) the property editor: Common / All, Go to group, the Template Options button; the page number */
|
|
1004
|
+
.pe-more { display: contents; }
|
|
1005
|
+
.pe-show-common [data-more] { display: none; }
|
|
1006
|
+
.pe-show-common fieldset.pe-group-more { display: none; }
|
|
1007
|
+
.pe-view { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 8px 6px; }
|
|
1008
|
+
.pe-view .segmented { margin-top: 0; }
|
|
1009
|
+
.pe-view .segmented button { font-size: 12px; padding: 2px 8px; min-height: 0; }
|
|
1010
|
+
.pe-goto { font-size: 12px; max-width: 12rem; }
|
|
1011
|
+
.pe-options-open .btn { justify-self: start; }
|
|
1012
|
+
.pd-goto-no input { width: 5.5em; font-variant-numeric: tabular-nums; }
|
|
1013
|
+
/* on narrower screens the previous / next arrows and the page list do it */
|
|
1014
|
+
@media (max-width: 1279px) { .pd-goto-no { display: none; } }
|
package/public/builder.js
CHANGED
|
@@ -209,6 +209,132 @@
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// ---------------------------------------------------------------- the rendering tree's context menu (sprint 77)
|
|
213
|
+
// Right-click (or Shift+F10 / the menu key) on a region, item or button: create in or below it, duplicate,
|
|
214
|
+
// copy to another page, expand or collapse everything below, delete. Every entry is a link or a form post
|
|
215
|
+
// the server also answers without JavaScript.
|
|
216
|
+
function setupTreeMenu(tree) {
|
|
217
|
+
const pid = tree.dataset.pageId;
|
|
218
|
+
const base = `/builder/pages/${pid}`;
|
|
219
|
+
const token = document.querySelector('input[name="__csrf"]')?.value ?? '';
|
|
220
|
+
let menu = null;
|
|
221
|
+
let opener = null;
|
|
222
|
+
const close = (refocus = true) => {
|
|
223
|
+
menu?.remove();
|
|
224
|
+
menu = null;
|
|
225
|
+
if (refocus) opener?.focus();
|
|
226
|
+
};
|
|
227
|
+
const submit = (action, fields, confirmText) => {
|
|
228
|
+
if (confirmText && !window.confirm(confirmText)) return;
|
|
229
|
+
const f = document.createElement('form');
|
|
230
|
+
f.method = 'post';
|
|
231
|
+
f.action = action;
|
|
232
|
+
for (const [k, v] of Object.entries({ __csrf: token, ...fields })) {
|
|
233
|
+
const i = document.createElement('input');
|
|
234
|
+
i.type = 'hidden';
|
|
235
|
+
i.name = k;
|
|
236
|
+
i.value = v;
|
|
237
|
+
f.appendChild(i);
|
|
238
|
+
}
|
|
239
|
+
document.body.appendChild(f);
|
|
240
|
+
f.submit();
|
|
241
|
+
};
|
|
242
|
+
const setAll = (node, open) => {
|
|
243
|
+
const li = node.closest('li');
|
|
244
|
+
for (const sub of [li, ...li.querySelectorAll('li')]) {
|
|
245
|
+
const n = sub.querySelector(':scope > .pd-node.has-kids');
|
|
246
|
+
if (!n) continue;
|
|
247
|
+
sub.toggleAttribute('data-collapsed', !open);
|
|
248
|
+
n.setAttribute('aria-expanded', String(open));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const entries = (node) => {
|
|
252
|
+
const kind = node.dataset.kind;
|
|
253
|
+
const id = node.dataset.id;
|
|
254
|
+
const label = node.querySelector('.pd-label')?.textContent ?? '';
|
|
255
|
+
const go = (q) => () => window.location.assign(`${base}?${q}`);
|
|
256
|
+
const copyDialog = () => {
|
|
257
|
+
const d = document.getElementById('pd-copy-dialog');
|
|
258
|
+
if (!d) return;
|
|
259
|
+
d.querySelector('input[name="kind"]').value = kind;
|
|
260
|
+
d.querySelector('input[name="id"]').value = id;
|
|
261
|
+
d.querySelector('[data-copy-what]').textContent = `${kind[0].toUpperCase()}${kind.slice(1)}: ${label}`;
|
|
262
|
+
d.showModal();
|
|
263
|
+
};
|
|
264
|
+
const list = [];
|
|
265
|
+
if (kind === 'region') {
|
|
266
|
+
list.push(['Create page item', go(`new=item®ion=${id}`)], ['Create button', go(`new=button®ion=${id}`)],
|
|
267
|
+
['Create dynamic action', go('new=dynamic_action')],
|
|
268
|
+
['Create region below', () => submit(`${base}/layout/create`, { kind: 'region', type: 'static', position: node.dataset.position ?? '', before: node.dataset.next ?? '' })], null);
|
|
269
|
+
}
|
|
270
|
+
list.push(['Duplicate', () => submit(`${base}/layout/duplicate`, { kind, id })], ['Copy to other page…', copyDialog]);
|
|
271
|
+
if (kind !== 'region') list.push(['Create dynamic action', go('new=dynamic_action')]);
|
|
272
|
+
if (node.classList.contains('has-kids')) list.push(null, ['Expand all below', () => setAll(node, true)], ['Collapse all below', () => setAll(node, false)]);
|
|
273
|
+
list.push(null, ['Delete', () => submit(`${base}/c/${kind}/${id}/delete`, {}, `Delete ${kind} ${label}?`), 'danger']);
|
|
274
|
+
return list;
|
|
275
|
+
};
|
|
276
|
+
const open = (node, x, y) => {
|
|
277
|
+
close(false);
|
|
278
|
+
opener = node;
|
|
279
|
+
menu = document.createElement('div');
|
|
280
|
+
menu.className = 'pd-menu';
|
|
281
|
+
menu.setAttribute('role', 'menu');
|
|
282
|
+
menu.setAttribute('aria-label', `Actions: ${node.querySelector('.pd-label')?.textContent ?? ''}`);
|
|
283
|
+
for (const e of entries(node)) {
|
|
284
|
+
if (!e) {
|
|
285
|
+
const sep = document.createElement('div');
|
|
286
|
+
sep.setAttribute('role', 'separator');
|
|
287
|
+
menu.appendChild(sep);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const b = document.createElement('button');
|
|
291
|
+
b.type = 'button';
|
|
292
|
+
b.setAttribute('role', 'menuitem');
|
|
293
|
+
b.tabIndex = -1;
|
|
294
|
+
b.textContent = e[0];
|
|
295
|
+
if (e[2]) b.classList.add(`is-${e[2]}`);
|
|
296
|
+
b.addEventListener('click', () => {
|
|
297
|
+
close(e[0] !== 'Copy to other page…');
|
|
298
|
+
e[1]();
|
|
299
|
+
});
|
|
300
|
+
menu.appendChild(b);
|
|
301
|
+
}
|
|
302
|
+
document.body.appendChild(menu);
|
|
303
|
+
const r = menu.getBoundingClientRect();
|
|
304
|
+
menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - r.width - 4))}px`;
|
|
305
|
+
menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - r.height - 4))}px`;
|
|
306
|
+
menu.querySelector('[role=menuitem]')?.focus();
|
|
307
|
+
menu.addEventListener('keydown', (e) => {
|
|
308
|
+
const items = [...menu.querySelectorAll('[role=menuitem]')];
|
|
309
|
+
const i = items.indexOf(document.activeElement);
|
|
310
|
+
if (e.key === 'ArrowDown') items[(i + 1) % items.length].focus();
|
|
311
|
+
else if (e.key === 'ArrowUp') items[(i - 1 + items.length) % items.length].focus();
|
|
312
|
+
else if (e.key === 'Home') items[0].focus();
|
|
313
|
+
else if (e.key === 'End') items[items.length - 1].focus();
|
|
314
|
+
else if (e.key === 'Escape' || e.key === 'Tab') close();
|
|
315
|
+
else return;
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
tree.addEventListener('contextmenu', (e) => {
|
|
320
|
+
const node = e.target.closest('.pd-node[data-kind][data-id]');
|
|
321
|
+
if (!node || !['region', 'item', 'button'].includes(node.dataset.kind)) return;
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
const r = node.getBoundingClientRect();
|
|
324
|
+
// the keyboard's menu key has no pointer position
|
|
325
|
+
open(node, e.clientX || r.left + 24, e.clientY || r.bottom);
|
|
326
|
+
});
|
|
327
|
+
document.addEventListener('pointerdown', (e) => {
|
|
328
|
+
if (menu && !menu.contains(e.target)) close(false);
|
|
329
|
+
});
|
|
330
|
+
window.addEventListener('blur', () => close(false));
|
|
331
|
+
const dialog = document.getElementById('pd-copy-dialog');
|
|
332
|
+
dialog?.querySelector('[data-dialog-close]')?.addEventListener('click', () => {
|
|
333
|
+
dialog.close();
|
|
334
|
+
opener?.focus();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
212
338
|
// ---------------------------------------------------------------- property editor
|
|
213
339
|
function setupProperties() {
|
|
214
340
|
const pe = document.querySelector('.pe');
|
|
@@ -269,6 +395,102 @@
|
|
|
269
395
|
}
|
|
270
396
|
});
|
|
271
397
|
}
|
|
398
|
+
// (sprint 77, APEX: Show Common / Show All, Go to Group) next to the filter
|
|
399
|
+
if (row && groups.length) {
|
|
400
|
+
const bar = document.createElement('div');
|
|
401
|
+
bar.className = 'pe-view';
|
|
402
|
+
if (pe.querySelector('[data-more]')) {
|
|
403
|
+
const seg = document.createElement('div');
|
|
404
|
+
seg.className = 'segmented pe-common';
|
|
405
|
+
seg.setAttribute('role', 'group');
|
|
406
|
+
seg.setAttribute('aria-label', 'Properties shown');
|
|
407
|
+
const showCommon = (on, remember) => {
|
|
408
|
+
pe.classList.toggle('pe-show-common', on);
|
|
409
|
+
for (const b of seg.querySelectorAll('button')) b.setAttribute('aria-pressed', String((b.value === 'common') === on));
|
|
410
|
+
for (const fs of groups) fs.classList.toggle('pe-group-more', on && !fs.querySelector('.field:not([data-more] .field), .options-field:not([data-more] .options-field)'));
|
|
411
|
+
if (remember) store.set('pe.common', on ? '1' : null);
|
|
412
|
+
};
|
|
413
|
+
for (const [v, label] of [['common', 'Common'], ['all', 'All']]) {
|
|
414
|
+
const b = document.createElement('button');
|
|
415
|
+
b.type = 'button';
|
|
416
|
+
b.value = v;
|
|
417
|
+
b.textContent = label;
|
|
418
|
+
b.addEventListener('click', () => showCommon(v === 'common', true));
|
|
419
|
+
seg.appendChild(b);
|
|
420
|
+
}
|
|
421
|
+
bar.appendChild(seg);
|
|
422
|
+
showCommon(store.get('pe.common') === '1', false);
|
|
423
|
+
}
|
|
424
|
+
const named = groups.filter((fs) => fs.querySelector(':scope > legend'));
|
|
425
|
+
if (named.length > 1) {
|
|
426
|
+
const sel = document.createElement('select');
|
|
427
|
+
sel.className = 'pe-goto';
|
|
428
|
+
sel.setAttribute('aria-label', 'Go to group');
|
|
429
|
+
sel.innerHTML = '<option value="">Go to group</option>';
|
|
430
|
+
named.forEach((fs, i) => {
|
|
431
|
+
const o = document.createElement('option');
|
|
432
|
+
o.value = String(i);
|
|
433
|
+
o.textContent = fs.querySelector(':scope > legend').textContent.trim();
|
|
434
|
+
sel.appendChild(o);
|
|
435
|
+
});
|
|
436
|
+
sel.addEventListener('change', () => {
|
|
437
|
+
const fs = named[Number(sel.value)];
|
|
438
|
+
sel.value = '';
|
|
439
|
+
if (!fs) return;
|
|
440
|
+
// a group on another tab (a region's Attributes): open that tab first (tabify names its tab <panel id>-tab)
|
|
441
|
+
const panel = fs.closest('[data-tab]');
|
|
442
|
+
if (panel?.hidden) document.getElementById(`${panel.id}-tab`)?.click();
|
|
443
|
+
const t = fs.querySelector('.pe-toggle');
|
|
444
|
+
if (t?.getAttribute('aria-expanded') === 'false') t.click();
|
|
445
|
+
fs.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
446
|
+
t?.focus({ preventScroll: true });
|
|
447
|
+
});
|
|
448
|
+
bar.appendChild(sel);
|
|
449
|
+
}
|
|
450
|
+
if (bar.children.length) row.after(bar);
|
|
451
|
+
}
|
|
452
|
+
// (sprint 77, APEX: the Template Options dialog) the checkboxes in a dialog, opened from a button that counts them.
|
|
453
|
+
// The dialog is the body's (not inside the form or the property editor's wrappers, which Common hides); the
|
|
454
|
+
// checkboxes belong to their form through the form attribute, so they are saved with it.
|
|
455
|
+
let optionDialogs = 0;
|
|
456
|
+
for (const fs of pe.querySelectorAll('fieldset.options-field')) {
|
|
457
|
+
const form = fs.closest('form');
|
|
458
|
+
if (!form) continue;
|
|
459
|
+
if (!form.id) form.id = `pe-form-${++optionDialogs}`;
|
|
460
|
+
const boxes = [...fs.querySelectorAll('input[type=checkbox]')];
|
|
461
|
+
for (const x of boxes) x.setAttribute('form', form.id);
|
|
462
|
+
const legend = fs.querySelector('legend')?.textContent.trim() ?? 'Options';
|
|
463
|
+
const holder = document.createElement('div');
|
|
464
|
+
holder.className = 'field pe-options-open';
|
|
465
|
+
const b = document.createElement('button');
|
|
466
|
+
b.type = 'button';
|
|
467
|
+
b.className = 'btn btn-sm';
|
|
468
|
+
const count = () => {
|
|
469
|
+
const n = boxes.filter((x) => x.checked).length;
|
|
470
|
+
b.textContent = `${legend}${n ? ` (${n})` : ''}…`;
|
|
471
|
+
};
|
|
472
|
+
const d = document.createElement('dialog');
|
|
473
|
+
d.className = 'pd-dialog';
|
|
474
|
+
d.setAttribute('aria-label', legend);
|
|
475
|
+
fs.replaceWith(holder);
|
|
476
|
+
holder.appendChild(b);
|
|
477
|
+
d.appendChild(fs);
|
|
478
|
+
const ok = document.createElement('div');
|
|
479
|
+
ok.className = 'pd-dialog-buttons';
|
|
480
|
+
ok.innerHTML = '<button type="button" class="btn btn-primary">OK</button>';
|
|
481
|
+
d.appendChild(ok);
|
|
482
|
+
document.body.appendChild(d);
|
|
483
|
+
// the count follows every change (Escape closes the dialog without OK); a change is an unsaved change
|
|
484
|
+
for (const x of boxes)
|
|
485
|
+
x.addEventListener('change', () => {
|
|
486
|
+
count();
|
|
487
|
+
document.querySelector(`.tb-save[form="${form.id}"]`)?.classList.add('is-dirty');
|
|
488
|
+
});
|
|
489
|
+
ok.querySelector('button').addEventListener('click', () => d.close());
|
|
490
|
+
d.addEventListener('close', () => b.focus());
|
|
491
|
+
b.addEventListener('click', () => d.showModal());
|
|
492
|
+
count();
|
|
493
|
+
}
|
|
272
494
|
if (head) {
|
|
273
495
|
const h = () => pe.style.setProperty('--pe-head-h', `${head.offsetHeight}px`);
|
|
274
496
|
h();
|
|
@@ -360,9 +582,12 @@
|
|
|
360
582
|
e.dataTransfer.effectAllowed = drag.isNew ? 'copy' : 'move';
|
|
361
583
|
e.dataTransfer.setData('text/plain', drag.isNew ? `new ${drag.kind} ${drag.type}` : `${drag.kind} ${drag.id}`);
|
|
362
584
|
(drag.kind === 'region' && !drag.isNew ? src.closest('.pd-region') : src).classList.add('is-dragging');
|
|
585
|
+
// (sprint 77) empty display positions and button positions show while something can drop there
|
|
586
|
+
canvas.classList.add(`pd-dragging-${drag.kind}`);
|
|
363
587
|
});
|
|
364
588
|
document.addEventListener('dragend', () => {
|
|
365
589
|
for (const el of document.querySelectorAll('.is-dragging')) el.classList.remove('is-dragging');
|
|
590
|
+
canvas.classList.remove('pd-dragging-region', 'pd-dragging-item', 'pd-dragging-button');
|
|
366
591
|
clear();
|
|
367
592
|
drag = null;
|
|
368
593
|
});
|
|
@@ -411,8 +636,10 @@
|
|
|
411
636
|
clear();
|
|
412
637
|
const before = t.before ? t.before.dataset.id : '';
|
|
413
638
|
const region = d.kind === 'region' ? '' : t.zone.dataset.region;
|
|
414
|
-
|
|
415
|
-
|
|
639
|
+
// a region's display position, a button's position in its region
|
|
640
|
+
const place = d.kind === 'region' ? { position: t.zone.dataset.position ?? '' } : d.kind === 'button' ? { slot: t.zone.dataset.slot ?? '' } : {};
|
|
641
|
+
if (d.isNew) post('create', { kind: d.kind, type: d.type, region, before, ...place });
|
|
642
|
+
else if (before !== d.id || t.zone !== canvas.querySelector(`[data-kind="${d.kind}"][data-id="${d.id}"]`)?.closest('.pd-grid, .pd-slot')) post('move', { kind: d.kind, id: d.id, region, before, ...place });
|
|
416
643
|
});
|
|
417
644
|
|
|
418
645
|
// ------------------------------------------------ resizing a region (drag its right edge)
|
|
@@ -476,7 +703,7 @@
|
|
|
476
703
|
// ---------------------------------------------------------------- small things
|
|
477
704
|
// a select that navigates (the toolbar's page switcher)
|
|
478
705
|
document.addEventListener('change', (e) => {
|
|
479
|
-
if (e.target.matches?.('select[data-autosubmit]')) e.target.form?.submit();
|
|
706
|
+
if (e.target.matches?.('select[data-autosubmit], input[type=checkbox][data-autosubmit]')) e.target.form?.submit();
|
|
480
707
|
});
|
|
481
708
|
|
|
482
709
|
// a search box that filters a list as you type (the App Builder's applications);
|
|
@@ -776,6 +1003,7 @@
|
|
|
776
1003
|
setupThemePreview();
|
|
777
1004
|
setupTabs();
|
|
778
1005
|
for (const tree of document.querySelectorAll('.pd-tree')) setupTree(tree);
|
|
1006
|
+
for (const tree of document.querySelectorAll('.pd-tree[data-tree-menu]')) setupTreeMenu(tree);
|
|
779
1007
|
setupProperties();
|
|
780
1008
|
setupCanvas();
|
|
781
1009
|
}
|
|
@@ -890,3 +1118,52 @@ document.addEventListener('submit', (e) => {
|
|
|
890
1118
|
// back from the browser's cache (the back button): no overlay
|
|
891
1119
|
window.addEventListener('pageshow', (ev) => { if (ev.persisted) overlay.remove(); }, { once: true });
|
|
892
1120
|
});
|
|
1121
|
+
|
|
1122
|
+
// (sprint 74) a search box that filters a list's rows while typing (data-filter-rows="tbody id"); the form still works without it
|
|
1123
|
+
document.querySelectorAll('input[data-filter-rows]').forEach((el) => {
|
|
1124
|
+
const body = document.getElementById(el.dataset.filterRows);
|
|
1125
|
+
if (!body) return;
|
|
1126
|
+
el.addEventListener('input', () => {
|
|
1127
|
+
const q = el.value.trim().toLowerCase();
|
|
1128
|
+
body.querySelectorAll(':scope > tr, :scope > li').forEach((tr) => { tr.hidden = !!q && !tr.textContent.toLowerCase().includes(q); });
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// (sprint 74) Settings tabs without a reload; the form remembers the tab for the redirect after Save.
|
|
1133
|
+
// The authentication type shows only its own fields.
|
|
1134
|
+
(() => {
|
|
1135
|
+
const links = [...document.querySelectorAll('[data-tab-link]')];
|
|
1136
|
+
if (!links.length) return;
|
|
1137
|
+
const show = (tab) => {
|
|
1138
|
+
document.querySelectorAll('.settings-panel').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
|
|
1139
|
+
links.forEach((l) => (l.dataset.tabLink === tab ? l.setAttribute('aria-current', 'page') : l.removeAttribute('aria-current')));
|
|
1140
|
+
document.querySelectorAll('[data-settings-tab]').forEach((i) => { i.value = tab; });
|
|
1141
|
+
const save = document.querySelector('.settings-save');
|
|
1142
|
+
if (save) save.hidden = tab === 'errors' || tab === 'pwa';
|
|
1143
|
+
history.replaceState(null, '', `?tab=${tab}`);
|
|
1144
|
+
};
|
|
1145
|
+
links.forEach((l) => l.addEventListener('click', (e) => { e.preventDefault(); show(l.dataset.tabLink); }));
|
|
1146
|
+
const auth = document.querySelector('.settings-form select[name="authentication"]');
|
|
1147
|
+
auth?.addEventListener('change', () => {
|
|
1148
|
+
document.querySelectorAll('.auth-part').forEach((p) => { p.hidden = p.dataset.auth !== auth.value; });
|
|
1149
|
+
});
|
|
1150
|
+
})();
|
|
1151
|
+
|
|
1152
|
+
// (sprint 75) the Object Browser's filter: objects whose name holds the text; folders without any are hidden
|
|
1153
|
+
document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
|
|
1154
|
+
const tree = document.getElementById(el.dataset.filterTree);
|
|
1155
|
+
if (!tree) return;
|
|
1156
|
+
el.addEventListener('input', () => {
|
|
1157
|
+
const q = el.value.trim().toLowerCase();
|
|
1158
|
+
tree.querySelectorAll('details.ob-folder').forEach((folder) => {
|
|
1159
|
+
let n = 0;
|
|
1160
|
+
folder.querySelectorAll('li').forEach((li) => {
|
|
1161
|
+
const hit = !q || li.textContent.toLowerCase().includes(q);
|
|
1162
|
+
li.hidden = !hit;
|
|
1163
|
+
if (hit) n++;
|
|
1164
|
+
});
|
|
1165
|
+
folder.hidden = !!q && n === 0;
|
|
1166
|
+
if (q && n) folder.open = true;
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
});
|