209 lines
8.2 KiB
JavaScript
209 lines
8.2 KiB
JavaScript
/**
|
|
* flex-objects — custom web component field for the plugin-settings form.
|
|
*
|
|
* The field's value is an array of blueprint URLs (e.g.
|
|
* "blueprints://flex-objects/grav-pages.yaml") identifying which flex
|
|
* directories the plugin should expose. Renders one row per available
|
|
* blueprint with a segmented Enabled/Disabled toggle, matching the
|
|
* admin-classic UI semantics. Old saved values may still use legacy URLs
|
|
* (pages.yaml vs grav-pages.yaml etc.) — the API includes the legacy alias
|
|
* for each entry so we match either form.
|
|
*/
|
|
|
|
const TAG = window.__GRAV_FIELD_TAG;
|
|
|
|
class FlexObjectsField extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._value = []; // current array of enabled blueprint URLs
|
|
this._field = null; // blueprint field definition
|
|
this._items = null; // available blueprints fetched from API
|
|
this._loading = false;
|
|
this._error = null;
|
|
}
|
|
|
|
set field(v) { this._field = v; }
|
|
get field() { return this._field; }
|
|
|
|
set value(v) {
|
|
const arr = Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
|
|
this._value = arr;
|
|
if (this.isConnected && this._items) this._render();
|
|
}
|
|
get value() { return this._value; }
|
|
|
|
connectedCallback() {
|
|
this._render();
|
|
this._fetchBlueprints();
|
|
}
|
|
|
|
// ─── API helpers ───────────────────────────────────────────────────────
|
|
_apiUrl(path) {
|
|
return (window.__GRAV_API_SERVER_URL || '') +
|
|
(window.__GRAV_API_PREFIX || '/api/v1') + path;
|
|
}
|
|
|
|
_headers() {
|
|
const h = { Accept: 'application/json' };
|
|
const token = window.__GRAV_API_TOKEN;
|
|
if (token) h['X-API-Token'] = token;
|
|
return h;
|
|
}
|
|
|
|
async _fetchBlueprints() {
|
|
this._loading = true;
|
|
this._error = null;
|
|
try {
|
|
const resp = await fetch(this._apiUrl('/flex-objects/blueprints'), { headers: this._headers() });
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
const json = await resp.json();
|
|
this._items = Array.isArray(json.data) ? json.data : (Array.isArray(json) ? json : []);
|
|
} catch (e) {
|
|
this._error = e.message || String(e);
|
|
this._items = [];
|
|
} finally {
|
|
this._loading = false;
|
|
this._render();
|
|
}
|
|
}
|
|
|
|
// True when either the canonical or the legacy URL of `item` is in `value`.
|
|
_isEnabled(item) {
|
|
if (this._value.includes(item.url)) return true;
|
|
return item.legacy_url ? this._value.includes(item.legacy_url) : false;
|
|
}
|
|
|
|
_toggle(item, enabled) {
|
|
// Drop both forms first to keep the array clean, then add canonical if enabled
|
|
let next = this._value.filter((u) => u !== item.url && u !== item.legacy_url);
|
|
if (enabled) next.push(item.url);
|
|
this._value = next;
|
|
this._render();
|
|
this.dispatchEvent(new CustomEvent('change', { detail: next, bubbles: true }));
|
|
}
|
|
|
|
// ─── Rendering ─────────────────────────────────────────────────────────
|
|
_render() {
|
|
if (this._loading && !this._items) {
|
|
this.innerHTML = `<div class="fxo-status">Loading directories…</div>${this._styles()}`;
|
|
return;
|
|
}
|
|
if (this._error && !this._items?.length) {
|
|
this.innerHTML = `<div class="fxo-status fxo-error">Failed to load directories: ${this._escape(this._error)}</div>${this._styles()}`;
|
|
return;
|
|
}
|
|
if (!this._items?.length) {
|
|
this.innerHTML = `<div class="fxo-status">No flex directories available.</div>${this._styles()}`;
|
|
return;
|
|
}
|
|
|
|
const rows = this._items.map((item, i) => {
|
|
const enabled = this._isEnabled(item);
|
|
const id = `fxo-${this._uid}-${i}`;
|
|
const desc = item.description ? this._escape(item.description) : '';
|
|
return `
|
|
<div class="fxo-row">
|
|
<div class="fxo-meta">
|
|
<div class="fxo-title" ${desc ? `title="${desc}"` : ''}>${this._escape(item.title || item.type)}</div>
|
|
<div class="fxo-url">${this._escape(item.url)}</div>
|
|
</div>
|
|
<div class="fxo-toggle" role="group" aria-label="${this._escape(item.title || item.type)}">
|
|
<button type="button" data-id="${id}" data-enabled="1" class="${enabled ? 'is-on' : ''}">Enabled</button>
|
|
<button type="button" data-id="${id}" data-enabled="0" class="${!enabled ? 'is-on' : ''}">Disabled</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
this.innerHTML = `<div class="fxo-list">${rows}</div>${this._styles()}`;
|
|
|
|
this.querySelectorAll('.fxo-toggle button').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.dataset.id;
|
|
const wantEnabled = btn.dataset.enabled === '1';
|
|
const idx = parseInt(id.split('-').pop(), 10);
|
|
const item = this._items[idx];
|
|
if (!item) return;
|
|
if (this._isEnabled(item) === wantEnabled) return;
|
|
this._toggle(item, wantEnabled);
|
|
});
|
|
});
|
|
}
|
|
|
|
_styles() {
|
|
return `
|
|
<style>
|
|
.fxo-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.fxo-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border, #e5e7eb);
|
|
border-radius: 8px;
|
|
background: var(--muted, transparent);
|
|
}
|
|
.fxo-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
.fxo-title { font-size: 14px; font-weight: 500; color: var(--foreground, #1f2937); }
|
|
.fxo-url {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 11px;
|
|
color: var(--muted-foreground, #6b7280);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.fxo-toggle {
|
|
display: inline-flex;
|
|
border: 1px solid var(--border, #e5e7eb);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
background: var(--background, transparent);
|
|
}
|
|
.fxo-toggle button {
|
|
appearance: none;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 6px 14px;
|
|
font-size: 12px;
|
|
color: var(--muted-foreground, #6b7280);
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
.fxo-toggle button + button { border-left: 1px solid var(--border, #e5e7eb); }
|
|
.fxo-toggle button.is-on {
|
|
background: var(--primary, #3b82f6);
|
|
color: var(--primary-foreground, #fff);
|
|
}
|
|
.fxo-status {
|
|
padding: 12px;
|
|
color: var(--muted-foreground, #6b7280);
|
|
font-size: 13px;
|
|
}
|
|
.fxo-error { color: var(--destructive, #dc2626); }
|
|
</style>
|
|
`;
|
|
}
|
|
|
|
_escape(s) {
|
|
return String(s ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
get _uid() {
|
|
if (!this.__uid) this.__uid = Math.random().toString(36).slice(2, 8);
|
|
return this.__uid;
|
|
}
|
|
}
|
|
|
|
customElements.define(TAG, FlexObjectsField);
|