mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix: clamp colour components, and let the style editor actually take over (#569)
* fix(element-style): clamp out-of-range colour components instead of rejecting
A regression this framework shipped. The eight scoreboards used to read their
colours through sports_card.coerce_rgb, which clamps; routing them through the
shared element_color sent them through _normalize_color, which rejected any
component outside 0..255 and fell back to the default. So a configured
[999, -5, 20] -- a typo'd bright red -- rendered white instead of (255, 0, 20).
Their own test_element_text_colors.py caught it: one case of nineteen, in all
eight plugins, failing only once the core change reached main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(web): the style editor takes over its own blocks -- and gets to at all
Two defects, both found by rendering the real partial in a browser rather than
by reading the code.
It was losing a race to its own fields. The hand-off guard asked "do any
fallback controls differ from their server-rendered defaults?" as a proxy for
"is someone editing this?". But the fallback holds this block's own font
fields, and the font-selector widget populates them on the same 50ms timer --
so a plain page load, with nobody touching anything, raced into "dirty" and the
editor removed itself, leaving the 701-line accordion form it exists to
replace. Measured: seven customization.*.font selects dirty ~60ms after
injection, clean again by 400ms. The question is whether a *person* typed, and
event.isTrusted answers exactly that; the listeners now go on synchronously,
because the edit worth protecting can happen before initWidget runs.
It took over too much. Taking over removed the whole fallback section, but a
customization block can hold more than styling -- football keeps
favorite_result_colors there -- so that removed the only UI those fields had,
and the editor also rendered them as an element, giving every row an "enabled"
and three colour columns. Core now marks the blocks it recognises as styling
(the compact declaration already did; hand-written adoption did not), the
widget renders only those, and the template drops only the children the widget
reports owning.
Verified on football's real schema: 28 rows across four mode tabs, columns
Element/Font/Size/Colour/X/Y, favorite_result_colors still editable with its
ten inputs, no duplicated field names, no console errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(web): style editor no longer drops layout-only fields it never rendered
CodeRabbit flagged elementKeys() in style-editor.js: render() claims the
whole customization.layout child as the widget's own (removing it from the
generic fallback renderer, since posting the same offset twice is worse),
but elementKeys() only listed keys that also have their own top-level style
block. A hand-written schema can put a key under layout that never got one
-- a logo, a timeout indicator, a possession arrow with a position but no
font or colour -- and that key's only control silently disappeared: no row
in the style editor's table (elementKeys never listed it) and no fallback
section either (layout was removed wholesale).
elementKeys() now appends any layout-declared key not already covered by a
style element, so table() renders a row for it (layout columns only, no
style columns) and the wholesale layout ownership claim stays truthful.
Verified against current code before fixing. New regression test
(test/js/unit/test_style_editor_element_keys.js, following this repo's
existing eval-extraction pattern for testing widget JS without a browser)
fails against the reverted function and passes with the fix; added to
run_all.js and the suite table in test/js/README.md.
Full pytest suite: 4887 passed, 62 skipped, 2 failed -- both the
pre-existing Europe/Kiev/Asia/Calcutta tzdata-alias gap on this sandbox,
identical on origin/main, unrelated to this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpg3HLWohdCUdzz2QNHanm
* fix(web): style editor no longer strands leaf-valued layout fields
A prior fix on this PR made elementKeys() append any layout-only key with
no style block of its own (a logo, a timeout indicator, a possession
arrow), so table() draws a row for it instead of losing it when the
wholesale `layout` claim removes the generic fallback. That covers a
layout-only key shaped like an object (x_offset/y_offset, ...), because
columnsFor() only ever produced columns from a key's *sub-fields*.
It missed the case where the layout-only key's own value is itself a
leaf -- a plain "show_logo" boolean directly under layout, no x/y object
underneath. elementKeys() still lists it (any row: no matching column),
so it renders as an uneditable blank row and its only control -- the
generic fallback checkbox -- is still gone. Confirmed by executing the
real widget's render() against a synthetic schema in Node (a DOM-stub
harness, not committed): the field's name never appeared as an <input>.
columnsFor() now gives such a leaf key a column keyed to itself
('layout-leaf'), and elementRow() binds it to the leaf's own path
(customization.layout.<key>, matching the name the fallback would have
used) instead of leaving every cell blank.
New regression test (test/js/unit/test_style_editor_layout_leaf_columns.js,
following this PR's existing eval-extraction pattern) checks the leaf
column is produced, is self-keyed, doesn't duplicate, and that a schema
with no leaf-valued layout key is unaffected; wired into run_all.js and
the suite table in test/js/README.md.
test/js/run_all.js: 84 + 6 + 6 = all suites passed (jsdom unavailable
here, DOM suites skip as before). Python suite untouched by this change;
test_style_editor_extra_fields.py, test_style_editor_save_roundtrip.py
and the one PIL-dependent style_editor_takeover.py case fail identically
before this commit -- missing flask/PIL in this sandbox, not this PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(web): keep layout-leaf style-editor columns distinct from name collisions
columnsFor() keyed a layout-only leaf field's column by its bare field
name. If an unrelated element's style block or another element's layout
axis block happened to declare a sub-field with that same name, the
`!seen.has(key)` guard skipped creating the leaf's column, silently
dropping its only control again -- the same failure the leaf-column fix
was meant to close, just reached through a name collision (CodeRabbit
review on 324a7ea).
Key layout-leaf columns under a namespaced id so they can never be
shadowed by an unrelated column sharing their name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(web): CSS.escape() the owned key before it becomes a selector
container.dataset.ownedKeys round-trips schema property keys through a
DOM dataset attribute, and the takeover handoff spliced each one
straight into '[data-child-key="' + k + '"]' with no escaping --
inconsistent with this codebase's own convention elsewhere
(plugin-file-manager.js, app-shell.js's escapeCssSelector) for building
a selector from a dynamic value. A key containing a quote or backslash
would break the selector or be steerable; Codacy's static analysis
flagged this pattern (1 high ErrorProne finding on PR #569, current
head at the time) as a new issue, though its dashboard is unreachable
from this sandbox (egress to app.codacy.com is blocked) and the
check-run API returned no detail text -- verified and fixed by reading
the diff directly rather than the tool's own description.
Added a source-assertion regression test alongside this file's
existing ones (this behavior lives in an inline script no Python test
executes).
Full suite: 4888 passed, 62 skipped, 2 failed -- both the pre-existing
Europe/Kiev/Asia/Calcutta tzdata-alias gap in this sandbox, identical
on origin/main, unrelated to this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(web): every advertised layout offset gets a control in the style editor
The editor took the whole layout section over but matched offsets to style
rows by exact key. A hand-written schema's two blocks were never named alike --
football styles score_text but positions score -- so of football's eleven
positionable things only status_text had a control. Score, odds, both logos,
timeouts, possession, down-and-distance, date, time and records were options
the schema advertised and the renderer reads, reachable nowhere in the UI.
Core now resolves each style element's layout key through alias_keys, the map
the resolver already reads offsets with, and records it as x-layout-key. The
widget reads that rather than carrying a second copy of the rules, and posts
under the key the schema declares: football's own offset reader looks up
layout.score, so a value saved as layout.score_text would be kept and never
drawn. Layout entries no style element claims get an "Other positions" table
with its own columns, in every mode panel as well as the base one, in the order
the plugin declared them.
Verified in a browser against football's real schema: 92 of 92 layout fields
(23 base, 23 per mode) rendered exactly once under their declared names, none
posted under a style key, no duplicated field names, favorite_result_colors
still editable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,10 +146,20 @@
|
||||
function elementKeys(schema) {
|
||||
var props = (schema && schema.properties) || {};
|
||||
var order = schema['x-propertyOrder'] || Object.keys(props);
|
||||
return order.filter(function (k) {
|
||||
var shaped = order.filter(function (k) {
|
||||
return k !== 'layout' && k !== 'modes'
|
||||
&& own(props, k) && ownObj(props, k).properties;
|
||||
});
|
||||
// Core marks the blocks it recognises as styling. Prefer that: a
|
||||
// customization block can also hold a feature of its own -- football
|
||||
// keeps favorite_result_colors there -- and treating one as an element
|
||||
// gives every row that feature's fields as extra columns.
|
||||
var managed = shaped.filter(function (k) {
|
||||
return ownObj(props, k)['x-style-managed'] === true;
|
||||
});
|
||||
// Nothing marked means the schema never went through expansion, so
|
||||
// fall back to the shape test rather than rendering an empty table.
|
||||
return managed.length ? managed : shaped;
|
||||
}
|
||||
|
||||
function titleOf(schema, key) {
|
||||
@@ -357,25 +367,100 @@
|
||||
visible: '3.5rem'
|
||||
};
|
||||
|
||||
/**
|
||||
* Where an element's offsets live in the layout block, or null.
|
||||
*
|
||||
* A hand-written schema does not line up: football styles 'score_text'
|
||||
* but positions it under 'score'. Core resolves that through the same
|
||||
* alias map the renderer reads offsets with and records the answer as
|
||||
* x-layout-key, so the rules exist once, in element_style.py. Without
|
||||
* the annotation both blocks share one key, as the compact declaration
|
||||
* does.
|
||||
*/
|
||||
function layoutKeyFor(schema, key) {
|
||||
var props = (schema && schema.properties) || {};
|
||||
var layoutProps = ownObj(props, 'layout').properties || {};
|
||||
var declared = ownObj(props, key)['x-layout-key'];
|
||||
// Only an object-shaped entry holds offsets. A leaf straight under
|
||||
// layout (a show_logo toggle) is a control of its own, so it is never
|
||||
// claimed here and always gets a position row.
|
||||
if (typeof declared === 'string' && ownObj(layoutProps, declared).properties) {
|
||||
return declared;
|
||||
}
|
||||
return ownObj(layoutProps, key).properties ? key : null;
|
||||
}
|
||||
|
||||
/** One row per styled element, paired with its offsets if it has any. */
|
||||
function styleRows(schema) {
|
||||
return elementKeys(schema).map(function (key) {
|
||||
return { key: key, layoutKey: layoutKeyFor(schema, key) };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* One row per positioned thing that has no style block of its own.
|
||||
*
|
||||
* Football positions both logos, timeouts, possession, down-and-distance,
|
||||
* the date, the time and the records, none of which has a font or a
|
||||
* colour. The editor takes the whole layout section over, so anything
|
||||
* not drawn here has no control at all.
|
||||
*/
|
||||
function positionRows(schema) {
|
||||
var layout = ownObj((schema && schema.properties) || {}, 'layout');
|
||||
var layoutProps = layout.properties || {};
|
||||
var claimed = styleRows(schema).map(function (r) { return r.layoutKey; });
|
||||
var order = layout['x-propertyOrder'] || Object.keys(layoutProps);
|
||||
return order.filter(function (lk) {
|
||||
var entry = own(layoutProps, lk);
|
||||
// Object-shaped entries carry offsets; a leaf is itself the
|
||||
// control. Both need a row here, because the editor removes the
|
||||
// fallback's whole layout section.
|
||||
return entry && typeof entry === 'object' && claimed.indexOf(lk) === -1;
|
||||
}).map(function (lk) {
|
||||
return { key: null, layoutKey: lk };
|
||||
});
|
||||
}
|
||||
|
||||
function layoutTitleOf(schema, layoutKey) {
|
||||
var layoutProps = ownObj((schema && schema.properties) || {}, 'layout').properties || {};
|
||||
return ownObj(layoutProps, layoutKey).title || layoutKey.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns a table needs, derived from the schema rather than fixed.
|
||||
*
|
||||
* Two sources: the sub-fields elements declare (font, font_size,
|
||||
* text_color, visible, align) and the sub-fields their layout blocks
|
||||
* declare (x_offset, y_offset, scale).
|
||||
* Three sources: the sub-fields a row's style block declares (font,
|
||||
* font_size, text_color, visible, align), the sub-fields of its layout
|
||||
* entry (x_offset, y_offset, scale), and a layout entry whose own value
|
||||
* *is* the field -- a show_logo toggle straight under layout has no x/y
|
||||
* object underneath, so it gets a column keyed to itself.
|
||||
*
|
||||
* `rows` defaults to every row the schema produces, style and position.
|
||||
*/
|
||||
function columnsFor(schema) {
|
||||
function columnsFor(schema, rows) {
|
||||
var props = schema.properties || {};
|
||||
var layoutProps = ownObj(props, 'layout').properties || {};
|
||||
rows = rows || styleRows(schema).concat(positionRows(schema));
|
||||
// A Map, not an object: the keys are field names out of a schema, so
|
||||
// a field literally named "constructor" is a column like any other
|
||||
// and never touches a prototype.
|
||||
var seen = new Map();
|
||||
elementKeys(schema).forEach(function (key) {
|
||||
Object.keys(ownObj(props, key).properties || {}).forEach(
|
||||
function (f) { seen.set(f, 'element'); });
|
||||
Object.keys(ownObj(layoutProps, key).properties || {}).forEach(
|
||||
function (f) { seen.set(f, 'layout'); });
|
||||
rows.forEach(function (row) {
|
||||
if (row.key) {
|
||||
Object.keys(ownObj(props, row.key).properties || {}).forEach(
|
||||
function (f) { seen.set(f, 'element'); });
|
||||
}
|
||||
if (!row.layoutKey) { return; }
|
||||
var entry = own(layoutProps, row.layoutKey);
|
||||
if (entry && typeof entry === 'object' && entry.properties) {
|
||||
Object.keys(entry.properties).forEach(
|
||||
function (f) { seen.set(f, 'layout'); });
|
||||
} else if (entry && typeof entry === 'object') {
|
||||
// A leaf. Namespaced so an unrelated column sharing its name
|
||||
// -- another entry's "scale" axis, say -- can never shadow it
|
||||
// and leave this row's only control blank.
|
||||
seen.set('layout-leaf:' + row.layoutKey, 'layout-leaf');
|
||||
}
|
||||
});
|
||||
var known = COLUMN_ORDER.filter(function (f) { return seen.get(f); });
|
||||
// Anything the schema declares that this file has never heard of
|
||||
@@ -384,10 +469,12 @@
|
||||
return COLUMN_ORDER.indexOf(f) === -1;
|
||||
}).sort();
|
||||
return known.concat(extra).map(function (f) {
|
||||
var isLeaf = f.indexOf('layout-leaf:') === 0;
|
||||
var fieldKey = isLeaf ? f.slice('layout-leaf:'.length) : f;
|
||||
return {
|
||||
key: f,
|
||||
key: fieldKey,
|
||||
where: seen.get(f),
|
||||
label: own(COLUMN_LABELS, f) || f.replace(/_/g, ' ')
|
||||
label: own(COLUMN_LABELS, fieldKey) || fieldKey.replace(/_/g, ' ')
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -421,21 +508,22 @@
|
||||
|
||||
function elementRow(opts) {
|
||||
var schema = opts.schema;
|
||||
var key = opts.key;
|
||||
var key = opts.row.key; // null for a position-only row
|
||||
var layoutKey = opts.row.layoutKey; // null for a style-only row
|
||||
var value = opts.value;
|
||||
var optional = opts.optional;
|
||||
|
||||
var props = ownObj(schema.properties || {}, key).properties || {};
|
||||
var props = key ? (ownObj(schema.properties || {}, key).properties || {}) : {};
|
||||
var layoutProps = ownObj(schema.properties || {}, 'layout').properties || {};
|
||||
var axes = ownObj(layoutProps, key).properties || {};
|
||||
var axes = layoutKey ? (ownObj(layoutProps, layoutKey).properties || {}) : {};
|
||||
|
||||
var row = el('div', {
|
||||
class: 'style-editor-row grid items-center gap-2 py-1',
|
||||
'data-element': key
|
||||
'data-element': key || ('layout.' + layoutKey)
|
||||
});
|
||||
row.appendChild(el('div', {
|
||||
class: 'text-sm text-gray-700 style-editor-label',
|
||||
text: titleOf(schema, key)
|
||||
text: key ? titleOf(schema, key) : layoutTitleOf(schema, layoutKey)
|
||||
}));
|
||||
|
||||
var sizeInput = null;
|
||||
@@ -461,8 +549,12 @@
|
||||
}
|
||||
|
||||
opts.columns.forEach(function (col) {
|
||||
var isLeaf = col.where === 'layout-leaf';
|
||||
var inLayout = col.where === 'layout';
|
||||
var prop = inLayout ? own(axes, col.key) : own(props, col.key);
|
||||
// A leaf column belongs to the one position row named after it;
|
||||
// every other row leaves that cell blank.
|
||||
var prop = isLeaf ? (col.key === layoutKey ? own(layoutProps, layoutKey) : null)
|
||||
: inLayout ? own(axes, col.key) : own(props, col.key);
|
||||
var cell;
|
||||
if (!prop) {
|
||||
// This element does not declare that field; keep the grid
|
||||
@@ -470,13 +562,19 @@
|
||||
row.appendChild(el('span'));
|
||||
return;
|
||||
}
|
||||
var path = inLayout ? ['layout', key, col.key] : [key, col.key];
|
||||
var base = inLayout ? opts.layoutPrefix + '.' + key
|
||||
: opts.prefix + '.' + key;
|
||||
var path = isLeaf ? ['layout', layoutKey]
|
||||
: inLayout ? ['layout', layoutKey, col.key] : [key, col.key];
|
||||
// Posted under the key the schema declares, never the style key:
|
||||
// a plugin's own offset reader looks up layout.score, so a value
|
||||
// saved as layout.score_text would be kept and never drawn. A
|
||||
// leaf is the field itself, so its name has no sub-field suffix --
|
||||
// the same name the generic renderer would have posted.
|
||||
var base = (isLeaf || inLayout) ? opts.layoutPrefix + '.' + layoutKey
|
||||
: opts.prefix + '.' + key;
|
||||
var node = control({
|
||||
key: col.key,
|
||||
prop: prop,
|
||||
name: base + '.' + col.key,
|
||||
name: isLeaf ? base : base + '.' + col.key,
|
||||
current: effective(value, path, prop, optional),
|
||||
optional: optional,
|
||||
fonts: opts.fonts,
|
||||
@@ -500,11 +598,11 @@
|
||||
return row;
|
||||
}
|
||||
|
||||
function header(columns) {
|
||||
function header(columns, firstLabel) {
|
||||
var row = el('div', {
|
||||
class: 'style-editor-row style-editor-head grid gap-2 pb-1 mb-1 border-b border-gray-300'
|
||||
});
|
||||
['Element'].concat(columns.map(function (c) { return c.label; }))
|
||||
[firstLabel || 'Element'].concat(columns.map(function (c) { return c.label; }))
|
||||
.forEach(function (label) {
|
||||
row.appendChild(el('div', {
|
||||
class: 'text-xs font-semibold text-gray-500 uppercase',
|
||||
@@ -516,7 +614,9 @@
|
||||
|
||||
function table(opts) {
|
||||
var wrap = el('div', { class: 'style-editor-table' });
|
||||
var columns = columnsFor(opts.schema);
|
||||
// Columns per table, so the positions table does not inherit a Font
|
||||
// column and the style table does not grow a "home x offset" one.
|
||||
var columns = columnsFor(opts.schema, opts.rows);
|
||||
// Sized here rather than in CSS: the column count depends on what
|
||||
// the plugin declared.
|
||||
wrap.style.gridTemplateColumns = '';
|
||||
@@ -524,11 +624,11 @@
|
||||
'minmax(7rem, 1.4fr) ' + columns.map(function (c) {
|
||||
return COLUMN_WIDTHS[c.key] || '5rem';
|
||||
}).join(' '));
|
||||
wrap.appendChild(header(columns));
|
||||
elementKeys(opts.schema).forEach(function (key) {
|
||||
wrap.appendChild(header(columns, opts.firstLabel));
|
||||
opts.rows.forEach(function (row) {
|
||||
wrap.appendChild(elementRow({
|
||||
schema: opts.schema,
|
||||
key: key,
|
||||
row: row,
|
||||
columns: columns,
|
||||
prefix: opts.prefix,
|
||||
layoutPrefix: opts.prefix + '.layout',
|
||||
@@ -540,6 +640,29 @@
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** One panel's tables: the styled elements, then anything only positioned. */
|
||||
function panelBody(opts) {
|
||||
var body = el('div');
|
||||
var shared = {
|
||||
schema: opts.schema, prefix: opts.prefix, value: opts.value,
|
||||
fonts: opts.fonts, optional: opts.optional
|
||||
};
|
||||
var styled = styleRows(opts.schema);
|
||||
if (styled.length) {
|
||||
body.appendChild(table(Object.assign({ rows: styled }, shared)));
|
||||
}
|
||||
var positioned = positionRows(opts.schema);
|
||||
if (positioned.length) {
|
||||
body.appendChild(el('div', {
|
||||
class: 'text-xs font-semibold text-gray-500 uppercase mt-4 mb-1 style-editor-subhead',
|
||||
text: 'Other positions'
|
||||
}));
|
||||
body.appendChild(table(Object.assign(
|
||||
{ rows: positioned, firstLabel: 'Item' }, shared)));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// ---- widget ----------------------------------------------------------
|
||||
|
||||
window.LEDMatrixWidgets.register('style-editor', {
|
||||
@@ -555,6 +678,20 @@
|
||||
var root = el('div', { class: 'style-editor' });
|
||||
container.appendChild(root);
|
||||
|
||||
// Published synchronously, because the host reads it the moment
|
||||
// this returns while the panels below wait on the font catalog.
|
||||
// layout and modes count as ours: their fields appear as columns
|
||||
// in these rows, so leaving them to the generic renderer would
|
||||
// post every offset twice from two different controls.
|
||||
var owned = elementKeys(schema);
|
||||
if (ownObj(schema.properties || {}, 'layout').properties) {
|
||||
owned = owned.concat(['layout']);
|
||||
}
|
||||
if (ownObj(schema.properties || {}, 'modes').properties) {
|
||||
owned = owned.concat(['modes']);
|
||||
}
|
||||
container.dataset.ownedKeys = owned.join(',');
|
||||
|
||||
loadFonts().then(function (fonts) {
|
||||
var modeProps = ownObj(schema.properties || {}, 'modes').properties || {};
|
||||
var modes = Object.keys(modeProps);
|
||||
@@ -596,7 +733,7 @@
|
||||
tabs.appendChild(b);
|
||||
}
|
||||
|
||||
panel('__base__', table({
|
||||
panel('__base__', panelBody({
|
||||
schema: schema, prefix: base, value: current,
|
||||
fonts: fonts, optional: false
|
||||
}));
|
||||
@@ -610,7 +747,7 @@
|
||||
class: 'text-xs text-gray-500 mb-2',
|
||||
text: 'Anything left blank follows the "All modes" tab.'
|
||||
}));
|
||||
node.appendChild(table({
|
||||
node.appendChild(panelBody({
|
||||
schema: modeSchema,
|
||||
prefix: base + '.modes.' + mode,
|
||||
value: modeValue,
|
||||
|
||||
@@ -97,23 +97,30 @@
|
||||
var container = document.getElementById('{{ field_id }}_container');
|
||||
if (container) { container.remove(); }
|
||||
}
|
||||
function fallbackIsDirty(root) {
|
||||
if (!root) { return false; }
|
||||
var controls = root.querySelectorAll('input, select, textarea');
|
||||
for (var i = 0; i < controls.length; i++) {
|
||||
var c = controls[i];
|
||||
if (c.type === 'checkbox' || c.type === 'radio') {
|
||||
if (c.checked !== c.defaultChecked) { return true; }
|
||||
} else if (c.tagName === 'SELECT') {
|
||||
for (var j = 0; j < c.options.length; j++) {
|
||||
if (c.options[j].selected !== c.options[j].defaultSelected) { return true; }
|
||||
}
|
||||
} else if (c.value !== c.defaultValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Did a *person* type into the fallback before this widget
|
||||
// took over? Comparing values against their defaults cannot
|
||||
// answer that. The fallback holds this block's own font
|
||||
// fields, and the font-selector widget populates them on the
|
||||
// same 50ms timer -- so a plain page load with nobody
|
||||
// touching anything raced into "dirty" (measured: seven
|
||||
// customization.*.font selects, ~60ms in) and silently
|
||||
// removed the style editor, leaving the accordion form it
|
||||
// exists to replace.
|
||||
//
|
||||
// isTrusted is false for programmatic changes and true only
|
||||
// for real input, which is exactly the question. The
|
||||
// listeners go on now, synchronously, because the edit to
|
||||
// protect can happen before initWidget ever runs.
|
||||
var userEdited = false;
|
||||
(function watchForRealEdits() {
|
||||
var fb = document.getElementById('{{ field_id }}_fallback');
|
||||
if (!fb) { return; }
|
||||
['input', 'change'].forEach(function (evt) {
|
||||
fb.addEventListener(evt, function (e) {
|
||||
if (e && e.isTrusted) { userEdited = true; }
|
||||
}, true);
|
||||
});
|
||||
})();
|
||||
function initWidget() {
|
||||
var widget = window.LEDMatrixWidgets && window.LEDMatrixWidgets.get('style-editor');
|
||||
if (!widget) {
|
||||
@@ -128,7 +135,7 @@
|
||||
// while this widget's script is still loading; swapping
|
||||
// in the widget now would replace them with the stale
|
||||
// server-rendered value and silently drop that edit.
|
||||
if (fallbackIsDirty(fallback)) { container.remove(); return; }
|
||||
if (userEdited) { container.remove(); return; }
|
||||
var value = {{ obj_value|tojson|safe }};
|
||||
var config = { schema: {{ prop|tojson|safe }} };
|
||||
try {
|
||||
@@ -142,9 +149,32 @@
|
||||
container.remove();
|
||||
return;
|
||||
}
|
||||
// The widget owns these fields now; drop the generic
|
||||
// rendering so the form does not post both.
|
||||
if (fallback) { fallback.remove(); }
|
||||
// The widget owns the style blocks now, so drop those from
|
||||
// the generic rendering or the form would post both. Only
|
||||
// those: a customization block can also hold a feature of
|
||||
// its own (football keeps favorite_result_colors there),
|
||||
// and removing the section wholesale took the only UI that
|
||||
// feature had with it.
|
||||
if (fallback) {
|
||||
var owned = (container.dataset.ownedKeys || '')
|
||||
.split(',').filter(function (k) { return k; });
|
||||
owned.forEach(function (k) {
|
||||
// CSS.escape: k is a schema property key read
|
||||
// back off dataset.ownedKeys, not a literal --
|
||||
// building the attribute selector by raw
|
||||
// concatenation would break (or be steerable)
|
||||
// on a key containing a quote or backslash.
|
||||
var node = fallback.querySelector(
|
||||
'[data-child-key="' + CSS.escape(k) + '"]');
|
||||
if (node) { node.remove(); }
|
||||
});
|
||||
// Nothing left but the section heading, or a widget
|
||||
// that reported nothing: drop the lot, as before.
|
||||
if (!owned.length
|
||||
|| !fallback.querySelector('[data-child-key]')) {
|
||||
fallback.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initWidget);
|
||||
@@ -1091,7 +1121,12 @@
|
||||
{% if nested_key in prop.properties %}
|
||||
{% set nested_prop = prop.properties[nested_key] %}
|
||||
{% set nested_val = nested_value[nested_key] if nested_key in nested_value else none %}
|
||||
{{ render_field(nested_key, nested_prop, nested_val, full_key, plugin_id) }}
|
||||
{# Tagged so a widget that takes over part of a block can
|
||||
drop exactly the children it owns and leave the rest
|
||||
rendered, rather than removing the whole section. #}
|
||||
<div data-child-key="{{ nested_key }}">
|
||||
{{ render_field(nested_key, nested_prop, nested_val, full_key, plugin_id) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user