mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
merge: fold the #569 review fixes into the alias-aware layout rows
Four commits landed on this branch from review while the layout wiring was in progress, and the two solved overlapping problems differently. Theirs made elementKeys() append every layout-only key, matched by exact name. Against football that also lists `score` and `odds` -- entries the alias resolution already pairs with score_text and odds_text -- so those offsets would get two controls posting the same field. The row split here (styleRows via core's x-layout-key, positionRows for what nothing claims) replaces that append; elementKeys() is back to listing style elements only. Theirs also covered cases this side had missed, all kept: - A leaf straight under layout (a show_logo toggle, no x/y object) now gets a position row with a self-keyed column bound to customization.layout.<key>. Core no longer lets an element claim a leaf as its offsets. - Leaf columns keep their namespaced id, so a leaf sharing a name with another entry's axis cannot be shadowed. - columnsFor(schema) still works with one argument (every row), so their column tests run unchanged. - The owned key is CSS.escape()'d before it becomes a selector. test_style_editor_element_keys.js is rewritten for the new contract, and says why the old one had to go. All six JS suites pass; in a browser football's 92 layout fields each render once, and a synthetic leaf -- including one named `scale` alongside a `scale` axis -- gets exactly one correctly named control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -780,8 +780,14 @@ def _adopt_handwritten_block(schema: Dict[str, Any],
|
|||||||
# editor matched them by exact name, so it drew one offset in eleven
|
# editor matched them by exact name, so it drew one offset in eleven
|
||||||
# and the rest had no control anywhere. Recording the answer here
|
# and the rest had no control anywhere. Recording the answer here
|
||||||
# keeps the alias rules in one place instead of a JavaScript copy.
|
# keeps the alias rules in one place instead of a JavaScript copy.
|
||||||
|
# Only an object-shaped entry (x_offset/y_offset/...) can hold an
|
||||||
|
# element's offsets. A leaf directly under layout -- a show_logo
|
||||||
|
# toggle -- is its own control, so it is never claimed by a row and
|
||||||
|
# always gets a position row of its own.
|
||||||
layout_key = next((name for name in alias_keys(key)
|
layout_key = next((name for name in alias_keys(key)
|
||||||
if isinstance(layout_fields.get(name), dict)), None)
|
if isinstance(layout_fields.get(name), dict)
|
||||||
|
and isinstance(layout_fields[name].get('properties'),
|
||||||
|
dict)), None)
|
||||||
if layout_key is not None:
|
if layout_key is not None:
|
||||||
props[key]['x-layout-key'] = layout_key
|
props[key]['x-layout-key'] = layout_key
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ nothing is listening, so it stays useful in a bare checkout.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `unit/test_list_filter.js` | no | `ListFilter` search/filter/sort/count/sticky, and the installed-plugins config **extracted verbatim** from `plugins_manager.js` so the test can't drift from it |
|
| `unit/test_list_filter.js` | no | `ListFilter` search/filter/sort/count/sticky, and the installed-plugins config **extracted verbatim** from `plugins_manager.js` so the test can't drift from it |
|
||||||
| `unit/test_render_cards.js` | no | `renderInstalledCards` markup, both empty states, and HTML-escaping of hostile plugin metadata |
|
| `unit/test_render_cards.js` | no | `renderInstalledCards` markup, both empty states, and HTML-escaping of hostile plugin metadata |
|
||||||
|
| `unit/test_style_editor_element_keys.js` | no | `elementKeys()`/`styleRows()`/`positionRows()` from `widgets/style-editor.js`: every `customization.layout` entry gets exactly one row -- paired with its style element through core's `x-layout-key` (so `score` belongs to `score_text`, not a second row), or a position row of its own, leaves included -- since the widget claims the whole `layout` block from the generic fallback renderer |
|
||||||
|
| `unit/test_style_editor_layout_leaf_columns.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only key whose own value is a leaf (no x/y sub-object, e.g. a `show_logo` toggle) gets a self-keyed column instead of a blank, uneditable row |
|
||||||
|
| `unit/test_style_editor_layout_leaf_collision.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only leaf key still gets its own column even when its name collides with an unrelated element's style sub-field or another layout axis's sub-field |
|
||||||
| `dom/test_installed_dom.js` | yes | The toolbar in a real DOM: pill/search/sort interaction, the HTMX partial re-swap, and a `getComputedStyle` check that `.filter-pill[data-active]` really matches the emitted markup |
|
| `dom/test_installed_dom.js` | yes | The toolbar in a real DOM: pill/search/sort interaction, the HTMX partial re-swap, and a `getComputedStyle` check that `.filter-pill[data-active]` really matches the emitted markup |
|
||||||
| `dom/test_store_dom.js` | yes | Store pagination, per-page, category, tri-state Installed button, and persistence across a re-boot, against the live registry |
|
| `dom/test_store_dom.js` | yes | Store pagination, per-page, category, tri-state Installed button, and persistence across a re-boot, against the live registry |
|
||||||
| `dom/test_no_double_fetch.js` | yes | Loads the **whole** `plugins_manager.js` and counts requests: typing in the store search must filter the cached list, not refetch `/api/v3/plugins/store/list` |
|
| `dom/test_no_double_fetch.js` | yes | Loads the **whole** `plugins_manager.js` and counts requests: typing in the store search must filter the cached list, not refetch `/api/v3/plugins/store/list` |
|
||||||
|
|||||||
+3
-1
@@ -15,7 +15,9 @@ const fs = require('fs');
|
|||||||
|
|
||||||
const BASE = process.env.BASE || 'http://localhost:5000';
|
const BASE = process.env.BASE || 'http://localhost:5000';
|
||||||
const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js',
|
const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js',
|
||||||
'unit/test_html_escaping.js'];
|
'unit/test_html_escaping.js', 'unit/test_style_editor_element_keys.js',
|
||||||
|
'unit/test_style_editor_layout_leaf_columns.js',
|
||||||
|
'unit/test_style_editor_layout_leaf_collision.js'];
|
||||||
const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js',
|
const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js',
|
||||||
'dom/test_tools_sections.js'];
|
'dom/test_tools_sections.js'];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Regression test for how style-editor.js decides which rows to draw,
|
||||||
|
// extracted verbatim from the shipped widget (own/ownObj/elementKeys/
|
||||||
|
// styleRows/positionRows have no DOM dependency) so the test can't drift from
|
||||||
|
// the real implementation.
|
||||||
|
//
|
||||||
|
// render() claims the whole `layout` child of a customization schema as the
|
||||||
|
// widget's own -- removing it from the generic fallback renderer entirely,
|
||||||
|
// because posting the same offset twice from two controls is worse than
|
||||||
|
// owning slightly too much. That is only safe when every entry under `layout`
|
||||||
|
// gets exactly one row:
|
||||||
|
//
|
||||||
|
// * styleRows(): one per style element, paired with the layout entry that
|
||||||
|
// holds its offsets. The pairing is not by exact name -- a hand-written
|
||||||
|
// schema styles 'score_text' but positions 'score' -- so core records it
|
||||||
|
// as x-layout-key, resolved through the same alias map the renderer reads
|
||||||
|
// offsets with.
|
||||||
|
// * positionRows(): one per layout entry no style element claimed -- a logo,
|
||||||
|
// a timeout indicator, a possession arrow, or a bare leaf like a
|
||||||
|
// show_logo toggle.
|
||||||
|
//
|
||||||
|
// This contract replaced an earlier one in which elementKeys() itself
|
||||||
|
// appended layout-only keys. That compared names exactly, so an aliased entry
|
||||||
|
// ('score' for 'score_text') was listed a second time and its offsets got two
|
||||||
|
// controls posting the same field.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const V3 = path.resolve(__dirname, '../../../web_interface/static/v3');
|
||||||
|
|
||||||
|
const src = fs.readFileSync(path.join(V3, 'js/widgets/style-editor.js'), 'utf8');
|
||||||
|
const start = src.indexOf('function own(obj, key)');
|
||||||
|
const endMark = 'function control(opts)';
|
||||||
|
const end = src.indexOf(endMark);
|
||||||
|
if (start === -1 || end === -1) {
|
||||||
|
console.error('FAIL: could not locate own()..positionRows() in style-editor.js -- slice markers need updating');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
eval(src.slice(start, end)); // defines own/ownObj/elementKeys/styleRows/positionRows
|
||||||
|
|
||||||
|
let failures = 0;
|
||||||
|
function ok(desc, cond) {
|
||||||
|
if (cond) { console.log(`ok - ${desc}`); }
|
||||||
|
else { console.log(`FAIL - ${desc}`); failures++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutKeysOf(rows) { return rows.map(function (r) { return r.layoutKey; }); }
|
||||||
|
|
||||||
|
// score_text is styled and positioned under an alias; odds_text has no
|
||||||
|
// offsets at all; possession is positioned but never styled; show_logo is a
|
||||||
|
// bare leaf under layout.
|
||||||
|
const schema = {
|
||||||
|
properties: {
|
||||||
|
score_text: {
|
||||||
|
type: 'object', 'x-style-managed': true, 'x-layout-key': 'score',
|
||||||
|
properties: { font: { type: 'string' }, text_color: { type: 'array' } },
|
||||||
|
},
|
||||||
|
odds_text: {
|
||||||
|
type: 'object', 'x-style-managed': true,
|
||||||
|
properties: { font: { type: 'string' } },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'object',
|
||||||
|
'x-propertyOrder': ['score', 'possession', 'show_logo'],
|
||||||
|
properties: {
|
||||||
|
score: { type: 'object', properties: { x_offset: {}, y_offset: {} } },
|
||||||
|
possession: { type: 'object', properties: { x_offset: {}, y_offset: {} } },
|
||||||
|
show_logo: { type: 'boolean', default: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = elementKeys(schema);
|
||||||
|
ok('elementKeys lists the style elements', keys.join(',') === 'score_text,odds_text');
|
||||||
|
ok('layout and modes containers are never treated as elements',
|
||||||
|
keys.indexOf('layout') === -1 && keys.indexOf('modes') === -1);
|
||||||
|
|
||||||
|
const styled = styleRows(schema);
|
||||||
|
ok('a style element is paired with the layout entry core resolved for it',
|
||||||
|
styled[0].key === 'score_text' && styled[0].layoutKey === 'score');
|
||||||
|
ok('a style element with no offsets claims nothing', styled[1].layoutKey === null);
|
||||||
|
|
||||||
|
const positions = positionRows(schema);
|
||||||
|
ok('a layout-only object entry gets a position row',
|
||||||
|
layoutKeysOf(positions).indexOf('possession') !== -1);
|
||||||
|
ok('a layout-only leaf gets a position row too',
|
||||||
|
layoutKeysOf(positions).indexOf('show_logo') !== -1);
|
||||||
|
ok('an entry claimed through an alias is not listed again as a position',
|
||||||
|
layoutKeysOf(positions).indexOf('score') === -1);
|
||||||
|
ok('positions follow the declared order',
|
||||||
|
layoutKeysOf(positions).join(',') === 'possession,show_logo');
|
||||||
|
|
||||||
|
const every = layoutKeysOf(styled).filter(Boolean).concat(layoutKeysOf(positions));
|
||||||
|
ok('every layout entry gets exactly one row',
|
||||||
|
every.length === new Set(every).size
|
||||||
|
&& every.slice().sort().join(',') === ['possession', 'score', 'show_logo'].join(','));
|
||||||
|
|
||||||
|
// A schema with no layout block at all must behave exactly as before --
|
||||||
|
// the overwhelmingly common case must not gain a phantom row.
|
||||||
|
const noLayout = {
|
||||||
|
properties: {
|
||||||
|
score_text: {
|
||||||
|
type: 'object', 'x-style-managed': true,
|
||||||
|
properties: { font: { type: 'string' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ok('a schema without a layout block has no positions',
|
||||||
|
elementKeys(noLayout).join(',') === 'score_text' && positionRows(noLayout).length === 0);
|
||||||
|
|
||||||
|
// Without an annotation (the compact declaration, or a schema that never went
|
||||||
|
// through expansion) the two blocks share one key.
|
||||||
|
const unannotated = {
|
||||||
|
properties: {
|
||||||
|
home_logo: { type: 'object', 'x-style-managed': true, properties: {} },
|
||||||
|
layout: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { home_logo: { type: 'object', properties: { x_offset: {}, scale: {} } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ok('an exact-name layout entry is claimed without an annotation',
|
||||||
|
styleRows(unannotated)[0].layoutKey === 'home_logo'
|
||||||
|
&& positionRows(unannotated).length === 0);
|
||||||
|
|
||||||
|
if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
|
||||||
|
console.log('\nall checks passed');
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Regression test for style-editor.js's columnsFor(), extracted verbatim
|
||||||
|
// from the shipped widget (see test_style_editor_layout_leaf_columns.js for
|
||||||
|
// the base case this builds on).
|
||||||
|
//
|
||||||
|
// A layout-only leaf key (e.g. a bare "scale" toggle directly under
|
||||||
|
// `layout`, no x/y object underneath) used to be keyed in the internal
|
||||||
|
// `seen` map by its own bare field name. If some *other* element's style
|
||||||
|
// block, or another element's layout axis block, happened to declare a
|
||||||
|
// sub-field with that exact same name (e.g. "scale" is also a real
|
||||||
|
// COLUMN_ORDER axis name most elements use), the `!seen.has(key)` guard
|
||||||
|
// skipped creating the leaf's column -- it silently reused the existing
|
||||||
|
// 'element'/'layout' column instead. That column's row binding in
|
||||||
|
// elementRow() then looked the field up under the wrong schema location for
|
||||||
|
// the leaf's own row, found nothing, and rendered a blank cell: the same
|
||||||
|
// "silently disappears" bug the leaf-column fix was meant to close, just
|
||||||
|
// reached through a name collision instead of a missing column altogether.
|
||||||
|
// This pins the fix: layout-leaf columns are keyed under a namespaced id so
|
||||||
|
// they can never be shadowed by an unrelated column sharing their name.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const V3 = path.resolve(__dirname, '../../../web_interface/static/v3');
|
||||||
|
|
||||||
|
const src = fs.readFileSync(path.join(V3, 'js/widgets/style-editor.js'), 'utf8');
|
||||||
|
const start = src.indexOf('function own(obj, key)');
|
||||||
|
const endMark = 'function control(opts)';
|
||||||
|
const end = src.indexOf(endMark);
|
||||||
|
if (start === -1 || end === -1) {
|
||||||
|
console.error('FAIL: could not locate own()/columnsFor() in style-editor.js -- slice markers need updating');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
eval(src.slice(start, end)); // defines own/ownObj/elementKeys/columnsFor
|
||||||
|
|
||||||
|
let failures = 0;
|
||||||
|
function ok(desc, cond) {
|
||||||
|
if (cond) { console.log(`ok - ${desc}`); }
|
||||||
|
else { console.log(`FAIL - ${desc}`); failures++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// `possession` is a real style element whose layout axis block declares a
|
||||||
|
// "scale" sub-field, which seeds seen.set('scale', 'layout'). `timeout_flag`
|
||||||
|
// is layout-only and a bare leaf valued directly under layout, but its own
|
||||||
|
// key is *also* "scale" -- an unrelated collision with that same name.
|
||||||
|
const schema = {
|
||||||
|
properties: {
|
||||||
|
possession: {
|
||||||
|
type: 'object',
|
||||||
|
'x-style-managed': true,
|
||||||
|
properties: { font: { type: 'string' } },
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
// The collision: a second element literally named "scale", entirely
|
||||||
|
// unrelated to the layout-only leaf below sharing that string.
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
possession: { type: 'object', properties: { scale: { type: 'number' } } },
|
||||||
|
scale: { type: 'boolean', default: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = columnsFor(schema);
|
||||||
|
const leafColumns = columns.filter(function (c) { return c.where === 'layout-leaf'; });
|
||||||
|
|
||||||
|
ok('the colliding layout axis sub-field still gets its own column',
|
||||||
|
columns.some(function (c) { return c.key === 'scale' && c.where === 'layout'; }));
|
||||||
|
ok('the layout-only leaf key still gets its own column despite the name collision',
|
||||||
|
leafColumns.length === 1 && leafColumns[0].key === 'scale');
|
||||||
|
ok('the leaf column is distinct from the colliding element/layout column',
|
||||||
|
columns.filter(function (c) { return c.key === 'scale'; }).length === 2);
|
||||||
|
|
||||||
|
if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
|
||||||
|
console.log('\nall checks passed');
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Regression test for style-editor.js's columnsFor(), extracted verbatim
|
||||||
|
// from the shipped widget (own/ownObj/elementKeys/columnsFor have no DOM
|
||||||
|
// dependency) so the test can't drift from the real implementation.
|
||||||
|
//
|
||||||
|
// elementKeys() already appends a layout-only key that never got its own
|
||||||
|
// top-level style block (see test_style_editor_element_keys.js), so table()
|
||||||
|
// draws a row for it. But render() still claims the whole `layout` child as
|
||||||
|
// the widget's own regardless of what that row actually shows -- and
|
||||||
|
// columnsFor() only produced a column for a layout-only key whose own value
|
||||||
|
// is an *object* with sub-fields (x_offset, y_offset, ...). A hand-written
|
||||||
|
// schema can instead put a plain leaf value directly under layout -- a
|
||||||
|
// "show_logo" toggle, a timeout indicator's on/off flag -- with no x/y
|
||||||
|
// object underneath it. That key still got a row (data-element="show_logo"),
|
||||||
|
// but zero columns ever matched it, so the row rendered with every cell
|
||||||
|
// blank: no control, and no way back to the fallback since layout was
|
||||||
|
// removed wholesale. This pins the fix: such a key gets its own
|
||||||
|
// self-keyed 'layout-leaf' column.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const V3 = path.resolve(__dirname, '../../../web_interface/static/v3');
|
||||||
|
|
||||||
|
const src = fs.readFileSync(path.join(V3, 'js/widgets/style-editor.js'), 'utf8');
|
||||||
|
const start = src.indexOf('function own(obj, key)');
|
||||||
|
const endMark = 'function control(opts)';
|
||||||
|
const end = src.indexOf(endMark);
|
||||||
|
if (start === -1 || end === -1) {
|
||||||
|
console.error('FAIL: could not locate own()/columnsFor() in style-editor.js -- slice markers need updating');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
eval(src.slice(start, end)); // defines own/ownObj/elementKeys/columnsFor
|
||||||
|
|
||||||
|
let failures = 0;
|
||||||
|
function ok(desc, cond) {
|
||||||
|
if (cond) { console.log(`ok - ${desc}`); }
|
||||||
|
else { console.log(`FAIL - ${desc}`); failures++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// score_text is a real style element; possession is layout-only but shaped
|
||||||
|
// like an object (x/y); show_logo is layout-only and a bare leaf value.
|
||||||
|
const schema = {
|
||||||
|
properties: {
|
||||||
|
score_text: {
|
||||||
|
type: 'object',
|
||||||
|
'x-style-managed': true,
|
||||||
|
properties: { font: { type: 'string' }, text_color: { type: 'array' } },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
score_text: { type: 'object', properties: { x_offset: {}, y_offset: {} } },
|
||||||
|
possession: { type: 'object', properties: { x_offset: {}, y_offset: {} } },
|
||||||
|
show_logo: { type: 'boolean', default: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = columnsFor(schema);
|
||||||
|
const byKey = {};
|
||||||
|
columns.forEach(function (c) { byKey[c.key] = c; });
|
||||||
|
|
||||||
|
ok('an element style field still gets its own column', byKey.font && byKey.font.where === 'element');
|
||||||
|
ok('a layout-only object key still contributes its x/y columns',
|
||||||
|
byKey.x_offset && byKey.x_offset.where === 'layout');
|
||||||
|
ok('a layout-only leaf key gets a column keyed to itself',
|
||||||
|
!!byKey.show_logo);
|
||||||
|
ok('...marked as a layout-leaf column, not a shared sub-field',
|
||||||
|
byKey.show_logo && byKey.show_logo.where === 'layout-leaf');
|
||||||
|
ok('nothing is duplicated', columns.length === new Set(columns.map(function (c) { return c.key; })).size);
|
||||||
|
|
||||||
|
// A schema with no leaf-valued layout key must not gain a phantom column --
|
||||||
|
// this is the common case (every existing scoreboard) and must be unaffected.
|
||||||
|
const noLeaf = {
|
||||||
|
properties: {
|
||||||
|
score_text: {
|
||||||
|
type: 'object', 'x-style-managed': true,
|
||||||
|
properties: { font: { type: 'string' } },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { score_text: { type: 'object', properties: { x_offset: {} } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ok('a schema with only object-shaped layout keys gets no layout-leaf column',
|
||||||
|
columnsFor(noLeaf).every(function (c) { return c.where !== 'layout-leaf'; }));
|
||||||
|
|
||||||
|
if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
|
||||||
|
console.log('\nall checks passed');
|
||||||
@@ -88,6 +88,16 @@ class TestItTakesOverOnlyItsOwnBlocks:
|
|||||||
entirely styling looks exactly as it did before."""
|
entirely styling looks exactly as it did before."""
|
||||||
assert "!fallback.querySelector('[data-child-key]')" in source
|
assert "!fallback.querySelector('[data-child-key]')" in source
|
||||||
|
|
||||||
|
def test_the_owned_key_is_escaped_before_it_becomes_a_selector(self, source):
|
||||||
|
"""ownedKeys comes back off a dataset attribute, not a literal --
|
||||||
|
splicing it into '[data-child-key="' + k + '"]' unescaped breaks (or
|
||||||
|
is steerable) on a key containing a quote or backslash. The rest of
|
||||||
|
this codebase (plugin-file-manager.js, app-shell.js) always routes a
|
||||||
|
dynamic value through CSS.escape() before it lands in a selector."""
|
||||||
|
assert "CSS.escape(k)" in source, (
|
||||||
|
"an owned key must be CSS.escape()'d before being interpolated "
|
||||||
|
"into the data-child-key attribute selector")
|
||||||
|
|
||||||
|
|
||||||
class TestTheSchemaSaysWhichBlocksAreStyling:
|
class TestTheSchemaSaysWhichBlocksAreStyling:
|
||||||
def test_adopted_blocks_are_marked_and_others_are_not(self):
|
def test_adopted_blocks_are_marked_and_others_are_not(self):
|
||||||
@@ -221,3 +231,21 @@ class TestEveryAdvertisedOffsetGetsAControl:
|
|||||||
"the alias rules live in element_style.py; the widget must read "
|
"the alias rules live in element_style.py; the widget must read "
|
||||||
"their result rather than carry a second copy")
|
"their result rather than carry a second copy")
|
||||||
assert "function positionRows" in js
|
assert "function positionRows" in js
|
||||||
|
|
||||||
|
def test_a_leaf_under_layout_is_never_claimed_as_offsets(self):
|
||||||
|
"""A show_logo toggle straight under layout has no x/y object, so it
|
||||||
|
cannot hold an element's offsets. Claiming it would give that row no
|
||||||
|
layout columns and remove the leaf from the positions list -- the
|
||||||
|
field would lose its only control."""
|
||||||
|
from src.element_style import expand_style_elements
|
||||||
|
|
||||||
|
schema = _football_shaped()
|
||||||
|
cust = schema["properties"]["customization"]
|
||||||
|
# A style element whose alias ("show_logo") names a leaf.
|
||||||
|
cust["properties"]["show_logo_text"] = _style_block()
|
||||||
|
cust["properties"]["layout"]["properties"]["show_logo"] = {
|
||||||
|
"type": "boolean", "default": True}
|
||||||
|
props = expand_style_elements(schema)["properties"]["customization"][
|
||||||
|
"properties"]
|
||||||
|
assert "x-layout-key" not in props["show_logo_text"]
|
||||||
|
assert "show_logo" in props["layout"]["x-propertyOrder"]
|
||||||
|
|||||||
@@ -367,13 +367,6 @@
|
|||||||
visible: '3.5rem'
|
visible: '3.5rem'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Where an element's offsets live in the layout block, or null.
|
* Where an element's offsets live in the layout block, or null.
|
||||||
*
|
*
|
||||||
@@ -388,10 +381,13 @@
|
|||||||
var props = (schema && schema.properties) || {};
|
var props = (schema && schema.properties) || {};
|
||||||
var layoutProps = ownObj(props, 'layout').properties || {};
|
var layoutProps = ownObj(props, 'layout').properties || {};
|
||||||
var declared = ownObj(props, key)['x-layout-key'];
|
var declared = ownObj(props, key)['x-layout-key'];
|
||||||
if (typeof declared === 'string' && own(layoutProps, declared)) {
|
// 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 declared;
|
||||||
}
|
}
|
||||||
return own(layoutProps, key) ? key : null;
|
return ownObj(layoutProps, key).properties ? key : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One row per styled element, paired with its offsets if it has any. */
|
/** One row per styled element, paired with its offsets if it has any. */
|
||||||
@@ -415,7 +411,11 @@
|
|||||||
var claimed = styleRows(schema).map(function (r) { return r.layoutKey; });
|
var claimed = styleRows(schema).map(function (r) { return r.layoutKey; });
|
||||||
var order = layout['x-propertyOrder'] || Object.keys(layoutProps);
|
var order = layout['x-propertyOrder'] || Object.keys(layoutProps);
|
||||||
return order.filter(function (lk) {
|
return order.filter(function (lk) {
|
||||||
return ownObj(layoutProps, lk).properties && claimed.indexOf(lk) === -1;
|
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) {
|
}).map(function (lk) {
|
||||||
return { key: null, layoutKey: lk };
|
return { key: null, layoutKey: lk };
|
||||||
});
|
});
|
||||||
@@ -426,9 +426,21 @@
|
|||||||
return ownObj(layoutProps, layoutKey).title || layoutKey.replace(/_/g, ' ');
|
return ownObj(layoutProps, layoutKey).title || layoutKey.replace(/_/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The columns a table needs, derived from the schema rather than fixed.
|
||||||
|
*
|
||||||
|
* 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, rows) {
|
function columnsFor(schema, rows) {
|
||||||
var props = schema.properties || {};
|
var props = schema.properties || {};
|
||||||
var layoutProps = ownObj(props, 'layout').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 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
|
// a field literally named "constructor" is a column like any other
|
||||||
// and never touches a prototype.
|
// and never touches a prototype.
|
||||||
@@ -438,9 +450,16 @@
|
|||||||
Object.keys(ownObj(props, row.key).properties || {}).forEach(
|
Object.keys(ownObj(props, row.key).properties || {}).forEach(
|
||||||
function (f) { seen.set(f, 'element'); });
|
function (f) { seen.set(f, 'element'); });
|
||||||
}
|
}
|
||||||
if (row.layoutKey) {
|
if (!row.layoutKey) { return; }
|
||||||
Object.keys(ownObj(layoutProps, row.layoutKey).properties || {}).forEach(
|
var entry = own(layoutProps, row.layoutKey);
|
||||||
|
if (entry && typeof entry === 'object' && entry.properties) {
|
||||||
|
Object.keys(entry.properties).forEach(
|
||||||
function (f) { seen.set(f, 'layout'); });
|
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); });
|
var known = COLUMN_ORDER.filter(function (f) { return seen.get(f); });
|
||||||
@@ -450,10 +469,12 @@
|
|||||||
return COLUMN_ORDER.indexOf(f) === -1;
|
return COLUMN_ORDER.indexOf(f) === -1;
|
||||||
}).sort();
|
}).sort();
|
||||||
return known.concat(extra).map(function (f) {
|
return known.concat(extra).map(function (f) {
|
||||||
|
var isLeaf = f.indexOf('layout-leaf:') === 0;
|
||||||
|
var fieldKey = isLeaf ? f.slice('layout-leaf:'.length) : f;
|
||||||
return {
|
return {
|
||||||
key: f,
|
key: fieldKey,
|
||||||
where: seen.get(f),
|
where: seen.get(f),
|
||||||
label: own(COLUMN_LABELS, f) || f.replace(/_/g, ' ')
|
label: own(COLUMN_LABELS, fieldKey) || fieldKey.replace(/_/g, ' ')
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -528,8 +549,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts.columns.forEach(function (col) {
|
opts.columns.forEach(function (col) {
|
||||||
|
var isLeaf = col.where === 'layout-leaf';
|
||||||
var inLayout = col.where === 'layout';
|
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;
|
var cell;
|
||||||
if (!prop) {
|
if (!prop) {
|
||||||
// This element does not declare that field; keep the grid
|
// This element does not declare that field; keep the grid
|
||||||
@@ -537,16 +562,19 @@
|
|||||||
row.appendChild(el('span'));
|
row.appendChild(el('span'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var path = inLayout ? ['layout', layoutKey, col.key] : [key, col.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:
|
// Posted under the key the schema declares, never the style key:
|
||||||
// a plugin's own offset reader looks up layout.score, so a value
|
// a plugin's own offset reader looks up layout.score, so a value
|
||||||
// saved as layout.score_text would be kept and never drawn.
|
// saved as layout.score_text would be kept and never drawn. A
|
||||||
var base = inLayout ? opts.layoutPrefix + '.' + layoutKey
|
// leaf is the field itself, so its name has no sub-field suffix --
|
||||||
: opts.prefix + '.' + key;
|
// the same name the generic renderer would have posted.
|
||||||
|
var base = (isLeaf || inLayout) ? opts.layoutPrefix + '.' + layoutKey
|
||||||
|
: opts.prefix + '.' + key;
|
||||||
var node = control({
|
var node = control({
|
||||||
key: col.key,
|
key: col.key,
|
||||||
prop: prop,
|
prop: prop,
|
||||||
name: base + '.' + col.key,
|
name: isLeaf ? base : base + '.' + col.key,
|
||||||
current: effective(value, path, prop, optional),
|
current: effective(value, path, prop, optional),
|
||||||
optional: optional,
|
optional: optional,
|
||||||
fonts: opts.fonts,
|
fonts: opts.fonts,
|
||||||
|
|||||||
@@ -159,8 +159,13 @@
|
|||||||
var owned = (container.dataset.ownedKeys || '')
|
var owned = (container.dataset.ownedKeys || '')
|
||||||
.split(',').filter(function (k) { return k; });
|
.split(',').filter(function (k) { return k; });
|
||||||
owned.forEach(function (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(
|
var node = fallback.querySelector(
|
||||||
'[data-child-key="' + k + '"]');
|
'[data-child-key="' + CSS.escape(k) + '"]');
|
||||||
if (node) { node.remove(); }
|
if (node) { node.remove(); }
|
||||||
});
|
});
|
||||||
// Nothing left but the section heading, or a widget
|
// Nothing left but the section heading, or a widget
|
||||||
|
|||||||
Reference in New Issue
Block a user