@jqhtml/core 2.3.68 → 2.3.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component.d.ts +12 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/debug-overlay/index.d.ts +6 -0
- package/dist/debug-overlay/index.d.ts.map +1 -1
- package/dist/index.cjs +256 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +256 -22
- package/dist/index.js.map +1 -1
- package/dist/jqhtml-core.esm.js +257 -23
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/jqhtml-core.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JQHTML Core v2.3.
|
|
2
|
+
* JQHTML Core v2.3.70
|
|
3
3
|
* (c) 2025 JQHTML Team
|
|
4
4
|
* Released under the MIT License
|
|
5
5
|
*/
|
|
@@ -1914,7 +1914,7 @@ Load_Coordinator._registry = new Map();
|
|
|
1914
1914
|
const class_registry = Object.create(null);
|
|
1915
1915
|
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
1916
1916
|
// Replaced at build time by rollup (see rollup.config.js).
|
|
1917
|
-
const CORE_VERSION = '2.3.
|
|
1917
|
+
const CORE_VERSION = '2.3.70';
|
|
1918
1918
|
const CLASS_MARKER = '__jqhtml_class__';
|
|
1919
1919
|
const PROPS_MARKER = '__jqhtml_props__';
|
|
1920
1920
|
/**
|
|
@@ -3943,6 +3943,7 @@ function resolve_parent_template(template_def, ctor) {
|
|
|
3943
3943
|
class Jqhtml_Component {
|
|
3944
3944
|
constructor(element, args = {}) {
|
|
3945
3945
|
this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
|
|
3946
|
+
this._debug_no_data = false; // Set by _reload_without_data(); cleared by any real load
|
|
3946
3947
|
this._instantiator = null; // Component that instantiated this one
|
|
3947
3948
|
this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
|
|
3948
3949
|
this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
|
|
@@ -4555,6 +4556,8 @@ class Jqhtml_Component {
|
|
|
4555
4556
|
// The queued executor can start long after load() was called - re-check.
|
|
4556
4557
|
if (this._stopped)
|
|
4557
4558
|
return false;
|
|
4559
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
4560
|
+
this._debug_no_data = false;
|
|
4558
4561
|
// Snapshot current data for change detection
|
|
4559
4562
|
const data_before = JSON.stringify(this.data);
|
|
4560
4563
|
// Execute on_load() on detached proxy (restores data to on_create snapshot)
|
|
@@ -4744,6 +4747,8 @@ class Jqhtml_Component {
|
|
|
4744
4747
|
async _load() {
|
|
4745
4748
|
if (this._stopped || this._ready_state >= 2)
|
|
4746
4749
|
return;
|
|
4750
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
4751
|
+
this._debug_no_data = false;
|
|
4747
4752
|
this._log_lifecycle('load', 'start');
|
|
4748
4753
|
// use_cached_data feature: If cache hit occurred and use_cached_data=true, skip on_load() entirely
|
|
4749
4754
|
// The cached data is already in this.data from create() phase
|
|
@@ -5176,6 +5181,51 @@ class Jqhtml_Component {
|
|
|
5176
5181
|
async refresh() {
|
|
5177
5182
|
return this.reload(false);
|
|
5178
5183
|
}
|
|
5184
|
+
/**
|
|
5185
|
+
* Re-render this component from its on_create() state with its cache dropped and
|
|
5186
|
+
* WITHOUT running on_load()/on_loaded()/on_ready().
|
|
5187
|
+
*
|
|
5188
|
+
* @internal debug tool. Used by the debug overlay's lifecycle footer to see what a
|
|
5189
|
+
* component looks like with no loaded data at all. Not public API, not documented,
|
|
5190
|
+
* and no downstream code should call it: reload() or refresh() afterwards returns
|
|
5191
|
+
* the component to normal (the snapshot is intact and the cache is gone, so the
|
|
5192
|
+
* next load is a genuine fresh fetch).
|
|
5193
|
+
*/
|
|
5194
|
+
_reload_without_data() {
|
|
5195
|
+
if (this._stopped)
|
|
5196
|
+
return Promise.resolve();
|
|
5197
|
+
// Same marker invalidation reload()/render() do: later ready() waiters must wait
|
|
5198
|
+
// for THIS cycle rather than resolve off the previous one.
|
|
5199
|
+
this.invalidate('ready');
|
|
5200
|
+
// Serialised with reload()/refresh() so it can never interleave with a real load.
|
|
5201
|
+
return this._queue.enqueue('reload', async () => {
|
|
5202
|
+
if (this._stopped)
|
|
5203
|
+
return;
|
|
5204
|
+
// Drop this component's own cache entries (data and html snapshot), so a later
|
|
5205
|
+
// reload() genuinely re-fetches instead of hydrating from what is on screen now.
|
|
5206
|
+
const { cache_key } = generate_cache_key(this);
|
|
5207
|
+
if (cache_key !== null) {
|
|
5208
|
+
Jqhtml_Local_Storage.remove(cache_key);
|
|
5209
|
+
Jqhtml_Local_Storage.remove(`${cache_key}::html`);
|
|
5210
|
+
}
|
|
5211
|
+
// Back to the on_create() state. Components with no custom on_load() take no
|
|
5212
|
+
// snapshot (nothing can have changed this.data), so they simply re-render.
|
|
5213
|
+
if (this.__initial_data_snapshot !== null) {
|
|
5214
|
+
this.__data_frozen = false;
|
|
5215
|
+
this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
|
|
5216
|
+
this.__data_frozen = true;
|
|
5217
|
+
}
|
|
5218
|
+
// Read by the debug overlay's lifecycle row; cleared by _reload()/load()/_load().
|
|
5219
|
+
this._debug_no_data = true;
|
|
5220
|
+
this._render();
|
|
5221
|
+
await this._wait_for_children_ready();
|
|
5222
|
+
if (this._stopped)
|
|
5223
|
+
return;
|
|
5224
|
+
this._ready_state = 4;
|
|
5225
|
+
this._update_debug_attrs();
|
|
5226
|
+
this.trigger('ready');
|
|
5227
|
+
});
|
|
5228
|
+
}
|
|
5179
5229
|
/**
|
|
5180
5230
|
* Internal reload implementation - re-fetch data and re-render
|
|
5181
5231
|
*
|
|
@@ -5213,6 +5263,8 @@ class Jqhtml_Component {
|
|
|
5213
5263
|
async _reload() {
|
|
5214
5264
|
if (this._stopped)
|
|
5215
5265
|
return;
|
|
5266
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
5267
|
+
this._debug_no_data = false;
|
|
5216
5268
|
// Invalidate ready event so new handlers wait for this reload to complete
|
|
5217
5269
|
// This prevents .on('ready') handlers from firing immediately based on previous lifecycle
|
|
5218
5270
|
this.invalidate('ready');
|
|
@@ -5951,9 +6003,9 @@ Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() det
|
|
|
5951
6003
|
// A bare component name, as accepted by closest() in place of a class selector
|
|
5952
6004
|
Jqhtml_Component.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
|
|
5953
6005
|
|
|
5954
|
-
var light_css = "html[data-jqhtml-debug] .jqhtml-debug-hit {\n outline: 2px solid #e5322d;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-0 {\n outline-color: #e5322d;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-1 {\n outline-color: #2f6fe4;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-2 {\n outline-color: #1f9d55;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-3 {\n outline-color: #d97706;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-4 {\n outline-color: #7c3aed;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-5 {\n outline-color: #0e7490;\n}";
|
|
6006
|
+
var light_css = "html[data-jqhtml-debug] .jqhtml-debug-selected {\n outline: 2px solid #b45309;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit {\n outline: 2px solid #e5322d;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-0 {\n outline-color: #e5322d;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-1 {\n outline-color: #2f6fe4;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-2 {\n outline-color: #1f9d55;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-3 {\n outline-color: #d97706;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-4 {\n outline-color: #7c3aed;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-5 {\n outline-color: #0e7490;\n}";
|
|
5955
6007
|
|
|
5956
|
-
var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 2147483647;\n pointer-events: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.4;\n color: #1c1c1c;\n text-align: left;\n text-transform: none;\n text-indent: 0;\n text-shadow: none;\n letter-spacing: normal;\n word-spacing: normal;\n white-space: normal;\n word-break: normal;\n overflow-wrap: anywhere;\n hyphens: none;\n tab-size: 4;\n direction: ltr;\n visibility: visible;\n cursor: default;\n list-style: none;\n quotes: none;\n}\n\n.jqhtml-debug-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label {\n position: absolute;\n box-sizing: border-box;\n display: block;\n padding: 1px 5px;\n min-height: 18px;\n color: #ffffff;\n background: #e5322d;\n border-radius: 0 0 3px 0;\n font-size: 11px;\n line-height: 16px;\n white-space: normal;\n overflow-wrap: anywhere;\n overflow: hidden;\n pointer-events:
|
|
6008
|
+
var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 2147483647;\n pointer-events: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.4;\n color: #1c1c1c;\n text-align: left;\n text-transform: none;\n text-indent: 0;\n text-shadow: none;\n letter-spacing: normal;\n word-spacing: normal;\n white-space: normal;\n word-break: normal;\n overflow-wrap: anywhere;\n hyphens: none;\n tab-size: 4;\n direction: ltr;\n visibility: visible;\n cursor: default;\n list-style: none;\n quotes: none;\n}\n\n.jqhtml-debug-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label {\n position: absolute;\n box-sizing: border-box;\n display: block;\n padding: 1px 5px;\n min-height: 18px;\n color: #ffffff;\n background: #e5322d;\n border-radius: 0 0 3px 0;\n font-size: 11px;\n line-height: 16px;\n white-space: normal;\n overflow-wrap: anywhere;\n overflow: hidden;\n pointer-events: auto;\n cursor: pointer;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-0 {\n background: #e5322d;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-1 {\n background: #2f6fe4;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-2 {\n background: #1f9d55;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-3 {\n background: #d97706;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-4 {\n background: #7c3aed;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-5 {\n background: #0e7490;\n}\n\n.jqhtml-debug-label-name {\n font-weight: bold;\n}\n\n.jqhtml-debug-label-args {\n opacity: 0.9;\n margin-left: 6px;\n}\n\n.jqhtml-debug-modal {\n display: none;\n position: absolute;\n box-sizing: border-box;\n top: 24px;\n right: 24px;\n width: 520px;\n max-width: calc(100% - 48px);\n overflow: hidden;\n padding: 0;\n background: #ffffff;\n color: #1c1c1c;\n border: 1px solid #d0d0d0;\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);\n pointer-events: auto;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-open {\n display: block;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-left {\n left: 24px;\n right: auto;\n}\n\n.jqhtml-debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #1c1c1c;\n color: #ffffff;\n font-weight: bold;\n}\n\n.jqhtml-debug-body {\n display: block;\n box-sizing: border-box;\n overflow-y: scroll;\n min-height: 300px;\n max-height: min(600px, 100vh - 200px);\n padding: 0 0 12px 0;\n}\n\n.jqhtml-debug-footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #ffffff;\n border-top: 1px solid #d0d0d0;\n}\n\n.jqhtml-debug-footer-label {\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-title-name {\n flex: 1 1 auto;\n font-size: 13px;\n}\n\n.jqhtml-debug-button {\n all: unset;\n display: inline-block;\n box-sizing: border-box;\n padding: 2px 8px;\n border: 1px solid rgba(255, 255, 255, 0.6);\n border-radius: 3px;\n color: #ffffff;\n font-size: 11px;\n line-height: 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.jqhtml-debug-button:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.jqhtml-debug-button.jqhtml-debug-footer-button {\n color: #1c1c1c;\n border-color: #d0d0d0;\n}\n\n.jqhtml-debug-button.jqhtml-debug-footer-button:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-section {\n padding: 8px 12px 0 12px;\n}\n\n.jqhtml-debug-h {\n margin: 4px 0;\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-table {\n display: grid;\n grid-template-columns: max-content max-content 1fr;\n column-gap: 12px;\n row-gap: 2px;\n align-items: baseline;\n}\n\n.jqhtml-debug-key {\n color: #005cc5;\n white-space: nowrap;\n}\n\n.jqhtml-debug-type {\n color: #666666;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.jqhtml-debug-value {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.jqhtml-debug-empty {\n color: #666666;\n font-style: italic;\n}\n\n.jqhtml-debug-list {\n display: block;\n margin: 0;\n padding: 0;\n}\n\n.jqhtml-debug-link {\n display: block;\n padding: 2px 4px;\n margin: 0 -4px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n.jqhtml-debug-link:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-link-cid {\n color: #666666;\n margin-left: 6px;\n}\n\n.jqhtml-debug-note {\n margin-top: 4px;\n color: #92400e;\n font-size: 11px;\n}";
|
|
5957
6009
|
|
|
5958
6010
|
/**
|
|
5959
6011
|
* Component debug overlay.
|
|
@@ -5965,6 +6017,12 @@ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed
|
|
|
5965
6017
|
* data, state, ancestry, instantiator) instead of firing the click. Alt+click
|
|
5966
6018
|
* passes through. `disable()` puts everything back.
|
|
5967
6019
|
*
|
|
6020
|
+
* The labels are part of the interaction, not just decoration: the pointer can
|
|
6021
|
+
* move from a component onto one of its tabs without the hover set collapsing,
|
|
6022
|
+
* and clicking a tab inspects that tab's component. The modal keeps an amber
|
|
6023
|
+
* outline on whatever it is showing, and its title bar carries Parent / Back so
|
|
6024
|
+
* the DOM chain can be walked without going back to the page.
|
|
6025
|
+
*
|
|
5968
6026
|
* Non-invasiveness, the design goal (details in ./CLAUDE.md):
|
|
5969
6027
|
* - The overlay's own UI lives in a shadow root on a host element appended to
|
|
5970
6028
|
* <body>, styled by shadow.scss. Page CSS cannot select into it and :host
|
|
@@ -5983,8 +6041,11 @@ const HOST_ATTR = 'data-jqhtml-debug-root'; // the shadow host
|
|
|
5983
6041
|
const LIGHT_STYLE_ID = 'jqhtml-debug-light-styles';
|
|
5984
6042
|
const HIT_CLASS = `${PREFIX}hit`;
|
|
5985
6043
|
const DEPTH_CLASS = `${PREFIX}depth-`;
|
|
6044
|
+
const SELECTED_CLASS = `${PREFIX}selected`; // on the element the modal is showing
|
|
5986
6045
|
const DEPTH_COLORS = 6; // keep in step with $depth-colors in _tokens.scss
|
|
5987
6046
|
const LABEL_HEIGHT = 18;
|
|
6047
|
+
const LABEL_MIN_WIDTH = 400; // a narrow component still gets a readable tab
|
|
6048
|
+
const VIEWPORT_MARGIN = 5; // gap kept between a label's right edge and the viewport
|
|
5988
6049
|
const READY_STATES = ['created', 'init', 'loaded', 'rendered', 'ready'];
|
|
5989
6050
|
const state = {
|
|
5990
6051
|
enabled: false,
|
|
@@ -5994,7 +6055,10 @@ const state = {
|
|
|
5994
6055
|
modal: null,
|
|
5995
6056
|
chain: [],
|
|
5996
6057
|
hit_elements: [],
|
|
6058
|
+
label_hovered: false,
|
|
5997
6059
|
modal_component: null,
|
|
6060
|
+
nav_stack: [],
|
|
6061
|
+
selected_element: null,
|
|
5998
6062
|
};
|
|
5999
6063
|
// ---------------------------------------------------------------------------
|
|
6000
6064
|
// Public API
|
|
@@ -6051,7 +6115,7 @@ const debug_overlay = {
|
|
|
6051
6115
|
: chain_for(target && target.jquery ? target[0] : target)[0];
|
|
6052
6116
|
if (!component)
|
|
6053
6117
|
return false;
|
|
6054
|
-
|
|
6118
|
+
select(component);
|
|
6055
6119
|
return true;
|
|
6056
6120
|
},
|
|
6057
6121
|
};
|
|
@@ -6106,9 +6170,18 @@ function is_ours(event) {
|
|
|
6106
6170
|
// ---------------------------------------------------------------------------
|
|
6107
6171
|
// Hover
|
|
6108
6172
|
// ---------------------------------------------------------------------------
|
|
6173
|
+
/**
|
|
6174
|
+
* A hover on the page replaces the hover set - except while the pointer is on
|
|
6175
|
+
* our own UI. An event from inside the shadow host (a label, the modal) never
|
|
6176
|
+
* clears or changes the hover, and neither does a page hover raised while the
|
|
6177
|
+
* pointer is parked on a label: that is what lets a label be reached and clicked
|
|
6178
|
+
* without the outlines it belongs to vanishing on the way.
|
|
6179
|
+
*/
|
|
6109
6180
|
function on_mouseover(event) {
|
|
6110
6181
|
if (is_ours(event))
|
|
6111
6182
|
return;
|
|
6183
|
+
if (state.label_hovered)
|
|
6184
|
+
return;
|
|
6112
6185
|
const chain = chain_for(event.target);
|
|
6113
6186
|
if (chain[0] === state.chain[0])
|
|
6114
6187
|
return;
|
|
@@ -6118,6 +6191,32 @@ function on_mouseout(event) {
|
|
|
6118
6191
|
if (!event.relatedTarget)
|
|
6119
6192
|
clear_hover(); // pointer left the document
|
|
6120
6193
|
}
|
|
6194
|
+
/**
|
|
6195
|
+
* The pointer left a label. If it landed back inside the component the label
|
|
6196
|
+
* belongs to (the usual case - the label sits on the component's own corner),
|
|
6197
|
+
* the hover set stays; anywhere else it goes. `elementFromPoint` reports our
|
|
6198
|
+
* shadow host for a point covered by another label, so the host is skipped and
|
|
6199
|
+
* the page element underneath is used.
|
|
6200
|
+
*/
|
|
6201
|
+
function on_label_leave(event) {
|
|
6202
|
+
state.label_hovered = false;
|
|
6203
|
+
const innermost = state.chain[0];
|
|
6204
|
+
if (!innermost)
|
|
6205
|
+
return;
|
|
6206
|
+
const under = element_under(event.clientX, event.clientY);
|
|
6207
|
+
if (!under || !innermost.$[0].contains(under))
|
|
6208
|
+
clear_hover();
|
|
6209
|
+
}
|
|
6210
|
+
function element_under(x, y) {
|
|
6211
|
+
const all = document.elementsFromPoint;
|
|
6212
|
+
const candidates = all
|
|
6213
|
+
? all.call(document, x, y)
|
|
6214
|
+
: [document.elementFromPoint(x, y)].filter(Boolean);
|
|
6215
|
+
for (const node of candidates)
|
|
6216
|
+
if (node !== state.host)
|
|
6217
|
+
return node;
|
|
6218
|
+
return null;
|
|
6219
|
+
}
|
|
6121
6220
|
function on_reposition() {
|
|
6122
6221
|
if (state.chain.length)
|
|
6123
6222
|
render_labels();
|
|
@@ -6140,18 +6239,23 @@ function clear_hover() {
|
|
|
6140
6239
|
}
|
|
6141
6240
|
state.hit_elements = [];
|
|
6142
6241
|
state.chain = [];
|
|
6242
|
+
state.label_hovered = false;
|
|
6143
6243
|
if (state.layer)
|
|
6144
6244
|
state.layer.textContent = '';
|
|
6145
6245
|
}
|
|
6146
6246
|
/**
|
|
6147
6247
|
* One tab per component in the chain, pinned to the component's top-left
|
|
6148
|
-
* corner
|
|
6149
|
-
*
|
|
6150
|
-
*
|
|
6248
|
+
* corner. Width: the component's, but a narrow component still gets up to
|
|
6249
|
+
* LABEL_MIN_WIDTH so its name and args stay legible; a label that would then
|
|
6250
|
+
* run off the right edge is slid back so its right edge sits at the margin.
|
|
6251
|
+
* Outermost first so the innermost paints on top; tabs that share a corner (a
|
|
6252
|
+
* child filling its parent, the usual case) stack downward instead of hiding
|
|
6253
|
+
* each other.
|
|
6151
6254
|
*/
|
|
6152
6255
|
function render_labels() {
|
|
6153
6256
|
const layer = state.layer;
|
|
6154
6257
|
layer.textContent = '';
|
|
6258
|
+
state.label_hovered = false; // the element the pointer was on is gone
|
|
6155
6259
|
const taken = new Map();
|
|
6156
6260
|
for (let depth = state.chain.length - 1; depth >= 0; depth--) {
|
|
6157
6261
|
const component = state.chain[depth];
|
|
@@ -6164,14 +6268,33 @@ function render_labels() {
|
|
|
6164
6268
|
const stacked = taken.get(key) || 0;
|
|
6165
6269
|
taken.set(key, stacked + 1);
|
|
6166
6270
|
const label = el('div', `label ${PREFIX}depth-${depth % DEPTH_COLORS}`);
|
|
6167
|
-
|
|
6271
|
+
// Laid out at the left edge FIRST, then moved. An absolutely positioned box
|
|
6272
|
+
// shrink-to-fits into the room its own `left` leaves, so measuring it at its
|
|
6273
|
+
// final position would report the squeezed width, and moving it would let it
|
|
6274
|
+
// re-expand - the two chase each other. At left 0 it has the whole viewport,
|
|
6275
|
+
// so the measurement below is the width it will actually keep.
|
|
6276
|
+
label.style.left = '0px';
|
|
6168
6277
|
label.style.top = `${top + stacked * LABEL_HEIGHT}px`;
|
|
6169
|
-
label.style.maxWidth = `${
|
|
6278
|
+
label.style.maxWidth = `${rect.width < LABEL_MIN_WIDTH ? LABEL_MIN_WIDTH : Math.round(rect.width)}px`;
|
|
6170
6279
|
label.appendChild(el('span', 'label-name', component.component_name()));
|
|
6171
6280
|
const args = simple_args(component);
|
|
6172
6281
|
if (args)
|
|
6173
6282
|
label.appendChild(el('span', 'label-args', args));
|
|
6283
|
+
// Each label knows its own component: it is a hover target that holds the set
|
|
6284
|
+
// in place, and a click target that inspects THAT component, not the innermost.
|
|
6285
|
+
label.addEventListener('mouseenter', () => { state.label_hovered = true; });
|
|
6286
|
+
label.addEventListener('mouseleave', on_label_leave);
|
|
6287
|
+
label.addEventListener('click', (event) => {
|
|
6288
|
+
event.stopPropagation(); // the document handler already skips is_ours events
|
|
6289
|
+
select(component);
|
|
6290
|
+
});
|
|
6174
6291
|
layer.appendChild(label);
|
|
6292
|
+
// A label on a narrow component may be wider than the component, so a component
|
|
6293
|
+
// near the right edge can push its label off-screen. Slide it back until its
|
|
6294
|
+
// right edge sits on the margin, never past the left edge.
|
|
6295
|
+
const width = label.getBoundingClientRect().width || label.offsetWidth;
|
|
6296
|
+
const limit = window.innerWidth - VIEWPORT_MARGIN;
|
|
6297
|
+
label.style.left = `${left + width > limit ? Math.max(0, Math.round(limit - width)) : left}px`;
|
|
6175
6298
|
}
|
|
6176
6299
|
}
|
|
6177
6300
|
/** `$id=7 $title="Users"` - only strings, numbers and booleans, never internals. */
|
|
@@ -6201,7 +6324,7 @@ function on_click(event) {
|
|
|
6201
6324
|
close_modal();
|
|
6202
6325
|
return;
|
|
6203
6326
|
}
|
|
6204
|
-
|
|
6327
|
+
select(chain[0]);
|
|
6205
6328
|
}
|
|
6206
6329
|
function on_keydown(event) {
|
|
6207
6330
|
if (event.key === 'Escape' && state.modal_component) {
|
|
@@ -6215,17 +6338,82 @@ function close_modal() {
|
|
|
6215
6338
|
state.modal.classList.remove(`${PREFIX}modal-open`);
|
|
6216
6339
|
state.modal.textContent = '';
|
|
6217
6340
|
state.modal_component = null;
|
|
6341
|
+
state.nav_stack = [];
|
|
6342
|
+
set_selected(null);
|
|
6343
|
+
}
|
|
6344
|
+
// ---------------------------------------------------------------------------
|
|
6345
|
+
// Selection and navigation
|
|
6346
|
+
//
|
|
6347
|
+
// Three ways in, and they differ only in what they do to the navigation stack:
|
|
6348
|
+
// select() - a FRESH selection (page click, label click, inspect()): the stack
|
|
6349
|
+
// is dropped, so Back disappears.
|
|
6350
|
+
// navigate() - a step along the chain (Parent, an Ancestry entry, the
|
|
6351
|
+
// Instantiator link): the component being left is pushed.
|
|
6352
|
+
// show() - render only, stack untouched; what Back and the lifecycle
|
|
6353
|
+
// buttons re-open with.
|
|
6354
|
+
// ---------------------------------------------------------------------------
|
|
6355
|
+
function select(component) {
|
|
6356
|
+
state.nav_stack = [];
|
|
6357
|
+
show(component);
|
|
6358
|
+
}
|
|
6359
|
+
function navigate(component) {
|
|
6360
|
+
if (state.modal_component && state.modal_component !== component) {
|
|
6361
|
+
state.nav_stack.push(state.modal_component);
|
|
6362
|
+
}
|
|
6363
|
+
show(component);
|
|
6218
6364
|
}
|
|
6219
|
-
|
|
6365
|
+
/** Pop to the last component still alive; a stopped one is skipped, not shown. */
|
|
6366
|
+
function go_back() {
|
|
6367
|
+
while (state.nav_stack.length) {
|
|
6368
|
+
const previous = state.nav_stack.pop();
|
|
6369
|
+
if (previous.$.hasClass('_Component_Stopped'))
|
|
6370
|
+
continue;
|
|
6371
|
+
show(previous);
|
|
6372
|
+
return;
|
|
6373
|
+
}
|
|
6374
|
+
if (state.modal_component)
|
|
6375
|
+
show(state.modal_component); // nothing left: drop Back
|
|
6376
|
+
}
|
|
6377
|
+
/** Move SELECTED_CLASS to `element`, or take it off the page entirely for null. */
|
|
6378
|
+
function set_selected(element) {
|
|
6379
|
+
if (state.selected_element && state.selected_element !== element) {
|
|
6380
|
+
state.selected_element.classList.remove(SELECTED_CLASS);
|
|
6381
|
+
}
|
|
6382
|
+
state.selected_element = element;
|
|
6383
|
+
if (element)
|
|
6384
|
+
element.classList.add(SELECTED_CLASS);
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* The inspector. Three children: a fixed title bar, a scrolling body holding every
|
|
6388
|
+
* section, and a fixed footer of lifecycle buttons. The modal sits top-right, but
|
|
6389
|
+
* moves to the left when the inspected component is itself in the right half of the
|
|
6390
|
+
* viewport - otherwise the panel covers the thing being inspected.
|
|
6391
|
+
*/
|
|
6392
|
+
function show(component) {
|
|
6220
6393
|
const modal = state.modal;
|
|
6221
6394
|
modal.textContent = '';
|
|
6222
6395
|
state.modal_component = component;
|
|
6223
6396
|
const chain = chain_for(component.$[0]);
|
|
6224
6397
|
const ancestors = chain.slice(1);
|
|
6225
6398
|
const element = component.$[0];
|
|
6226
|
-
|
|
6399
|
+
set_selected(element);
|
|
6400
|
+
modal.classList.toggle(`${PREFIX}modal-left`, element.getBoundingClientRect().left > window.innerWidth / 2);
|
|
6401
|
+
// Title bar: the name, then [Back] [Parent] [Log to console] [Close]. Back is
|
|
6402
|
+
// there only while something was navigated away from, Parent only while the
|
|
6403
|
+
// component has a DOM parent component - the first Ancestry entry.
|
|
6227
6404
|
const title = el('div', 'title');
|
|
6228
6405
|
title.appendChild(el('span', 'title-name', `<${component.component_name()}>`));
|
|
6406
|
+
if (state.nav_stack.length) {
|
|
6407
|
+
const back = el('button', 'button', 'Back');
|
|
6408
|
+
back.addEventListener('click', go_back);
|
|
6409
|
+
title.appendChild(back);
|
|
6410
|
+
}
|
|
6411
|
+
const parent = ancestors[0];
|
|
6412
|
+
if (parent) {
|
|
6413
|
+
const up = el('button', 'button', 'Parent');
|
|
6414
|
+
up.addEventListener('click', () => navigate(parent));
|
|
6415
|
+
title.appendChild(up);
|
|
6416
|
+
}
|
|
6229
6417
|
const log = el('button', 'button', 'Log to console');
|
|
6230
6418
|
log.addEventListener('click', () => {
|
|
6231
6419
|
console.log(`[JQHTML debug] <${component.component_name()}>`, component, element);
|
|
@@ -6235,6 +6423,9 @@ function open_modal(component) {
|
|
|
6235
6423
|
close.addEventListener('click', close_modal);
|
|
6236
6424
|
title.appendChild(close);
|
|
6237
6425
|
modal.appendChild(title);
|
|
6426
|
+
// The only scrolling element: the title bar and the footer stay put.
|
|
6427
|
+
const body = el('div', 'body');
|
|
6428
|
+
modal.appendChild(body);
|
|
6238
6429
|
// Identity
|
|
6239
6430
|
const ctor = component.constructor;
|
|
6240
6431
|
const hierarchy = ctor.get_class_hierarchy ? ctor.get_class_hierarchy() : [ctor.name];
|
|
@@ -6243,15 +6434,18 @@ function open_modal(component) {
|
|
|
6243
6434
|
row(identity, 'class', '', hierarchy.join(' → '));
|
|
6244
6435
|
row(identity, 'element', '', describe_element(element));
|
|
6245
6436
|
row(identity, '_cid', '', String(component._cid));
|
|
6246
|
-
|
|
6437
|
+
const lifecycle = READY_STATES[component._ready_state] || String(component._ready_state);
|
|
6438
|
+
row(identity, 'lifecycle', '', component._debug_no_data
|
|
6439
|
+
? `${lifecycle} (no data - reload w/o data)`
|
|
6440
|
+
: lifecycle);
|
|
6247
6441
|
const nocache = element.getAttribute('data-nocache');
|
|
6248
6442
|
if (nocache)
|
|
6249
6443
|
row(identity, 'cache', '', `declined: ${nocache}`);
|
|
6250
|
-
|
|
6444
|
+
body.appendChild(section('Identity', identity));
|
|
6251
6445
|
// Args / data / state
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6446
|
+
body.appendChild(section('Args', object_table(component.args)));
|
|
6447
|
+
body.appendChild(section('Data', object_table(component.data)));
|
|
6448
|
+
body.appendChild(section('State', object_table(component.state)));
|
|
6255
6449
|
// Ancestry
|
|
6256
6450
|
const list = el('div', 'list');
|
|
6257
6451
|
if (!ancestors.length)
|
|
@@ -6260,10 +6454,10 @@ function open_modal(component) {
|
|
|
6260
6454
|
const link = el('div', 'link');
|
|
6261
6455
|
link.appendChild(el('span', 'link-name', `<${ancestor.component_name()}>`));
|
|
6262
6456
|
link.appendChild(el('span', 'link-cid', String(ancestor._cid)));
|
|
6263
|
-
link.addEventListener('click', () =>
|
|
6457
|
+
link.addEventListener('click', () => navigate(ancestor));
|
|
6264
6458
|
list.appendChild(link);
|
|
6265
6459
|
}
|
|
6266
|
-
|
|
6460
|
+
body.appendChild(section('Ancestry (DOM, nearest first)', list));
|
|
6267
6461
|
// Instantiator - the component whose template wrote this tag. Differs from
|
|
6268
6462
|
// the DOM parent for markup written in a slot body or default content.
|
|
6269
6463
|
const instantiator = component.instantiator ? component.instantiator() : null;
|
|
@@ -6272,7 +6466,7 @@ function open_modal(component) {
|
|
|
6272
6466
|
const link = el('div', 'link');
|
|
6273
6467
|
link.appendChild(el('span', 'link-name', `<${instantiator.component_name()}>`));
|
|
6274
6468
|
link.appendChild(el('span', 'link-cid', String(instantiator._cid)));
|
|
6275
|
-
link.addEventListener('click', () =>
|
|
6469
|
+
link.addEventListener('click', () => navigate(instantiator));
|
|
6276
6470
|
inst.appendChild(link);
|
|
6277
6471
|
if (ancestors[0] && instantiator !== ancestors[0]) {
|
|
6278
6472
|
inst.appendChild(el('div', 'note', 'Written in this component\'s template but rendered inside a different parent (slot or default content): handlers and $sid bind here.'));
|
|
@@ -6281,9 +6475,49 @@ function open_modal(component) {
|
|
|
6281
6475
|
else {
|
|
6282
6476
|
inst.appendChild(el('div', 'empty', 'none - created directly'));
|
|
6283
6477
|
}
|
|
6284
|
-
|
|
6478
|
+
body.appendChild(section('Instantiator', inst));
|
|
6479
|
+
// Footer: lifecycle controls. Clicks land inside the shadow host, so the
|
|
6480
|
+
// overlay's own click hijack lets them through (is_ours).
|
|
6481
|
+
const footer = el('div', 'footer');
|
|
6482
|
+
footer.appendChild(el('span', 'footer-label', 'Lifecycle:'));
|
|
6483
|
+
footer.appendChild(lifecycle_button(component, 'Reload', (c) => c.reload()));
|
|
6484
|
+
footer.appendChild(lifecycle_button(component, 'Refresh', (c) => c.refresh()));
|
|
6485
|
+
footer.appendChild(lifecycle_button(component, 'Rerender', (c) => c.render()));
|
|
6486
|
+
footer.appendChild(lifecycle_button(component, 'Reload w/o data', (c) => c._reload_without_data()));
|
|
6487
|
+
modal.appendChild(footer);
|
|
6285
6488
|
modal.classList.add(`${PREFIX}modal-open`);
|
|
6286
6489
|
}
|
|
6490
|
+
/**
|
|
6491
|
+
* One footer button. Runs its lifecycle call on a live component, logs whatever it
|
|
6492
|
+
* throws or rejects with, and re-opens the modal once the call settles so the panel
|
|
6493
|
+
* (lifecycle row above all) describes the state the button just produced.
|
|
6494
|
+
*/
|
|
6495
|
+
function lifecycle_button(component, label, run) {
|
|
6496
|
+
const button = el('button', 'button footer-button', label);
|
|
6497
|
+
button.addEventListener('click', () => {
|
|
6498
|
+
if (component.$.hasClass('_Component_Stopped'))
|
|
6499
|
+
return; // dead component: nothing to do
|
|
6500
|
+
let result;
|
|
6501
|
+
try {
|
|
6502
|
+
result = run(component);
|
|
6503
|
+
}
|
|
6504
|
+
catch (error) {
|
|
6505
|
+
console.error(`[JQHTML debug] ${label} threw on <${component.component_name()}>`, error);
|
|
6506
|
+
return;
|
|
6507
|
+
}
|
|
6508
|
+
Promise.resolve(result)
|
|
6509
|
+
.catch((error) => {
|
|
6510
|
+
console.error(`[JQHTML debug] ${label} rejected on <${component.component_name()}>`, error);
|
|
6511
|
+
})
|
|
6512
|
+
.then(() => {
|
|
6513
|
+
// `show`, not `select`: the lifecycle buttons re-render the panel in place
|
|
6514
|
+
// and must not throw away the navigation stack behind it.
|
|
6515
|
+
if (state.enabled && state.modal_component === component)
|
|
6516
|
+
show(component);
|
|
6517
|
+
});
|
|
6518
|
+
});
|
|
6519
|
+
return button;
|
|
6520
|
+
}
|
|
6287
6521
|
// ---------------------------------------------------------------------------
|
|
6288
6522
|
// DOM helpers (shadow side)
|
|
6289
6523
|
// ---------------------------------------------------------------------------
|
|
@@ -7014,7 +7248,7 @@ function init(jQuery, config) {
|
|
|
7014
7248
|
}
|
|
7015
7249
|
}
|
|
7016
7250
|
// Version - will be replaced during build with actual version from package.json
|
|
7017
|
-
const version = '2.3.
|
|
7251
|
+
const version = '2.3.70';
|
|
7018
7252
|
// Default export with all functionality
|
|
7019
7253
|
const jqhtml = {
|
|
7020
7254
|
// Core
|