@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/index.js
CHANGED
|
@@ -1909,7 +1909,7 @@ Load_Coordinator._registry = new Map();
|
|
|
1909
1909
|
const class_registry = Object.create(null);
|
|
1910
1910
|
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
1911
1911
|
// Replaced at build time by rollup (see rollup.config.js).
|
|
1912
|
-
const CORE_VERSION = '2.3.
|
|
1912
|
+
const CORE_VERSION = '2.3.70';
|
|
1913
1913
|
const CLASS_MARKER = '__jqhtml_class__';
|
|
1914
1914
|
const PROPS_MARKER = '__jqhtml_props__';
|
|
1915
1915
|
/**
|
|
@@ -3938,6 +3938,7 @@ function resolve_parent_template(template_def, ctor) {
|
|
|
3938
3938
|
class Jqhtml_Component {
|
|
3939
3939
|
constructor(element, args = {}) {
|
|
3940
3940
|
this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
|
|
3941
|
+
this._debug_no_data = false; // Set by _reload_without_data(); cleared by any real load
|
|
3941
3942
|
this._instantiator = null; // Component that instantiated this one
|
|
3942
3943
|
this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
|
|
3943
3944
|
this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
|
|
@@ -4550,6 +4551,8 @@ class Jqhtml_Component {
|
|
|
4550
4551
|
// The queued executor can start long after load() was called - re-check.
|
|
4551
4552
|
if (this._stopped)
|
|
4552
4553
|
return false;
|
|
4554
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
4555
|
+
this._debug_no_data = false;
|
|
4553
4556
|
// Snapshot current data for change detection
|
|
4554
4557
|
const data_before = JSON.stringify(this.data);
|
|
4555
4558
|
// Execute on_load() on detached proxy (restores data to on_create snapshot)
|
|
@@ -4739,6 +4742,8 @@ class Jqhtml_Component {
|
|
|
4739
4742
|
async _load() {
|
|
4740
4743
|
if (this._stopped || this._ready_state >= 2)
|
|
4741
4744
|
return;
|
|
4745
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
4746
|
+
this._debug_no_data = false;
|
|
4742
4747
|
this._log_lifecycle('load', 'start');
|
|
4743
4748
|
// use_cached_data feature: If cache hit occurred and use_cached_data=true, skip on_load() entirely
|
|
4744
4749
|
// The cached data is already in this.data from create() phase
|
|
@@ -5171,6 +5176,51 @@ class Jqhtml_Component {
|
|
|
5171
5176
|
async refresh() {
|
|
5172
5177
|
return this.reload(false);
|
|
5173
5178
|
}
|
|
5179
|
+
/**
|
|
5180
|
+
* Re-render this component from its on_create() state with its cache dropped and
|
|
5181
|
+
* WITHOUT running on_load()/on_loaded()/on_ready().
|
|
5182
|
+
*
|
|
5183
|
+
* @internal debug tool. Used by the debug overlay's lifecycle footer to see what a
|
|
5184
|
+
* component looks like with no loaded data at all. Not public API, not documented,
|
|
5185
|
+
* and no downstream code should call it: reload() or refresh() afterwards returns
|
|
5186
|
+
* the component to normal (the snapshot is intact and the cache is gone, so the
|
|
5187
|
+
* next load is a genuine fresh fetch).
|
|
5188
|
+
*/
|
|
5189
|
+
_reload_without_data() {
|
|
5190
|
+
if (this._stopped)
|
|
5191
|
+
return Promise.resolve();
|
|
5192
|
+
// Same marker invalidation reload()/render() do: later ready() waiters must wait
|
|
5193
|
+
// for THIS cycle rather than resolve off the previous one.
|
|
5194
|
+
this.invalidate('ready');
|
|
5195
|
+
// Serialised with reload()/refresh() so it can never interleave with a real load.
|
|
5196
|
+
return this._queue.enqueue('reload', async () => {
|
|
5197
|
+
if (this._stopped)
|
|
5198
|
+
return;
|
|
5199
|
+
// Drop this component's own cache entries (data and html snapshot), so a later
|
|
5200
|
+
// reload() genuinely re-fetches instead of hydrating from what is on screen now.
|
|
5201
|
+
const { cache_key } = generate_cache_key(this);
|
|
5202
|
+
if (cache_key !== null) {
|
|
5203
|
+
Jqhtml_Local_Storage.remove(cache_key);
|
|
5204
|
+
Jqhtml_Local_Storage.remove(`${cache_key}::html`);
|
|
5205
|
+
}
|
|
5206
|
+
// Back to the on_create() state. Components with no custom on_load() take no
|
|
5207
|
+
// snapshot (nothing can have changed this.data), so they simply re-render.
|
|
5208
|
+
if (this.__initial_data_snapshot !== null) {
|
|
5209
|
+
this.__data_frozen = false;
|
|
5210
|
+
this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
|
|
5211
|
+
this.__data_frozen = true;
|
|
5212
|
+
}
|
|
5213
|
+
// Read by the debug overlay's lifecycle row; cleared by _reload()/load()/_load().
|
|
5214
|
+
this._debug_no_data = true;
|
|
5215
|
+
this._render();
|
|
5216
|
+
await this._wait_for_children_ready();
|
|
5217
|
+
if (this._stopped)
|
|
5218
|
+
return;
|
|
5219
|
+
this._ready_state = 4;
|
|
5220
|
+
this._update_debug_attrs();
|
|
5221
|
+
this.trigger('ready');
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5174
5224
|
/**
|
|
5175
5225
|
* Internal reload implementation - re-fetch data and re-render
|
|
5176
5226
|
*
|
|
@@ -5208,6 +5258,8 @@ class Jqhtml_Component {
|
|
|
5208
5258
|
async _reload() {
|
|
5209
5259
|
if (this._stopped)
|
|
5210
5260
|
return;
|
|
5261
|
+
// Any real load returns the component to normal, whatever the debug tool did.
|
|
5262
|
+
this._debug_no_data = false;
|
|
5211
5263
|
// Invalidate ready event so new handlers wait for this reload to complete
|
|
5212
5264
|
// This prevents .on('ready') handlers from firing immediately based on previous lifecycle
|
|
5213
5265
|
this.invalidate('ready');
|
|
@@ -5946,9 +5998,9 @@ Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() det
|
|
|
5946
5998
|
// A bare component name, as accepted by closest() in place of a class selector
|
|
5947
5999
|
Jqhtml_Component.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
|
|
5948
6000
|
|
|
5949
|
-
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}";
|
|
6001
|
+
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}";
|
|
5950
6002
|
|
|
5951
|
-
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:
|
|
6003
|
+
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}";
|
|
5952
6004
|
|
|
5953
6005
|
/**
|
|
5954
6006
|
* Component debug overlay.
|
|
@@ -5960,6 +6012,12 @@ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed
|
|
|
5960
6012
|
* data, state, ancestry, instantiator) instead of firing the click. Alt+click
|
|
5961
6013
|
* passes through. `disable()` puts everything back.
|
|
5962
6014
|
*
|
|
6015
|
+
* The labels are part of the interaction, not just decoration: the pointer can
|
|
6016
|
+
* move from a component onto one of its tabs without the hover set collapsing,
|
|
6017
|
+
* and clicking a tab inspects that tab's component. The modal keeps an amber
|
|
6018
|
+
* outline on whatever it is showing, and its title bar carries Parent / Back so
|
|
6019
|
+
* the DOM chain can be walked without going back to the page.
|
|
6020
|
+
*
|
|
5963
6021
|
* Non-invasiveness, the design goal (details in ./CLAUDE.md):
|
|
5964
6022
|
* - The overlay's own UI lives in a shadow root on a host element appended to
|
|
5965
6023
|
* <body>, styled by shadow.scss. Page CSS cannot select into it and :host
|
|
@@ -5978,8 +6036,11 @@ const HOST_ATTR = 'data-jqhtml-debug-root'; // the shadow host
|
|
|
5978
6036
|
const LIGHT_STYLE_ID = 'jqhtml-debug-light-styles';
|
|
5979
6037
|
const HIT_CLASS = `${PREFIX}hit`;
|
|
5980
6038
|
const DEPTH_CLASS = `${PREFIX}depth-`;
|
|
6039
|
+
const SELECTED_CLASS = `${PREFIX}selected`; // on the element the modal is showing
|
|
5981
6040
|
const DEPTH_COLORS = 6; // keep in step with $depth-colors in _tokens.scss
|
|
5982
6041
|
const LABEL_HEIGHT = 18;
|
|
6042
|
+
const LABEL_MIN_WIDTH = 400; // a narrow component still gets a readable tab
|
|
6043
|
+
const VIEWPORT_MARGIN = 5; // gap kept between a label's right edge and the viewport
|
|
5983
6044
|
const READY_STATES = ['created', 'init', 'loaded', 'rendered', 'ready'];
|
|
5984
6045
|
const state = {
|
|
5985
6046
|
enabled: false,
|
|
@@ -5989,7 +6050,10 @@ const state = {
|
|
|
5989
6050
|
modal: null,
|
|
5990
6051
|
chain: [],
|
|
5991
6052
|
hit_elements: [],
|
|
6053
|
+
label_hovered: false,
|
|
5992
6054
|
modal_component: null,
|
|
6055
|
+
nav_stack: [],
|
|
6056
|
+
selected_element: null,
|
|
5993
6057
|
};
|
|
5994
6058
|
// ---------------------------------------------------------------------------
|
|
5995
6059
|
// Public API
|
|
@@ -6046,7 +6110,7 @@ const debug_overlay = {
|
|
|
6046
6110
|
: chain_for(target && target.jquery ? target[0] : target)[0];
|
|
6047
6111
|
if (!component)
|
|
6048
6112
|
return false;
|
|
6049
|
-
|
|
6113
|
+
select(component);
|
|
6050
6114
|
return true;
|
|
6051
6115
|
},
|
|
6052
6116
|
};
|
|
@@ -6101,9 +6165,18 @@ function is_ours(event) {
|
|
|
6101
6165
|
// ---------------------------------------------------------------------------
|
|
6102
6166
|
// Hover
|
|
6103
6167
|
// ---------------------------------------------------------------------------
|
|
6168
|
+
/**
|
|
6169
|
+
* A hover on the page replaces the hover set - except while the pointer is on
|
|
6170
|
+
* our own UI. An event from inside the shadow host (a label, the modal) never
|
|
6171
|
+
* clears or changes the hover, and neither does a page hover raised while the
|
|
6172
|
+
* pointer is parked on a label: that is what lets a label be reached and clicked
|
|
6173
|
+
* without the outlines it belongs to vanishing on the way.
|
|
6174
|
+
*/
|
|
6104
6175
|
function on_mouseover(event) {
|
|
6105
6176
|
if (is_ours(event))
|
|
6106
6177
|
return;
|
|
6178
|
+
if (state.label_hovered)
|
|
6179
|
+
return;
|
|
6107
6180
|
const chain = chain_for(event.target);
|
|
6108
6181
|
if (chain[0] === state.chain[0])
|
|
6109
6182
|
return;
|
|
@@ -6113,6 +6186,32 @@ function on_mouseout(event) {
|
|
|
6113
6186
|
if (!event.relatedTarget)
|
|
6114
6187
|
clear_hover(); // pointer left the document
|
|
6115
6188
|
}
|
|
6189
|
+
/**
|
|
6190
|
+
* The pointer left a label. If it landed back inside the component the label
|
|
6191
|
+
* belongs to (the usual case - the label sits on the component's own corner),
|
|
6192
|
+
* the hover set stays; anywhere else it goes. `elementFromPoint` reports our
|
|
6193
|
+
* shadow host for a point covered by another label, so the host is skipped and
|
|
6194
|
+
* the page element underneath is used.
|
|
6195
|
+
*/
|
|
6196
|
+
function on_label_leave(event) {
|
|
6197
|
+
state.label_hovered = false;
|
|
6198
|
+
const innermost = state.chain[0];
|
|
6199
|
+
if (!innermost)
|
|
6200
|
+
return;
|
|
6201
|
+
const under = element_under(event.clientX, event.clientY);
|
|
6202
|
+
if (!under || !innermost.$[0].contains(under))
|
|
6203
|
+
clear_hover();
|
|
6204
|
+
}
|
|
6205
|
+
function element_under(x, y) {
|
|
6206
|
+
const all = document.elementsFromPoint;
|
|
6207
|
+
const candidates = all
|
|
6208
|
+
? all.call(document, x, y)
|
|
6209
|
+
: [document.elementFromPoint(x, y)].filter(Boolean);
|
|
6210
|
+
for (const node of candidates)
|
|
6211
|
+
if (node !== state.host)
|
|
6212
|
+
return node;
|
|
6213
|
+
return null;
|
|
6214
|
+
}
|
|
6116
6215
|
function on_reposition() {
|
|
6117
6216
|
if (state.chain.length)
|
|
6118
6217
|
render_labels();
|
|
@@ -6135,18 +6234,23 @@ function clear_hover() {
|
|
|
6135
6234
|
}
|
|
6136
6235
|
state.hit_elements = [];
|
|
6137
6236
|
state.chain = [];
|
|
6237
|
+
state.label_hovered = false;
|
|
6138
6238
|
if (state.layer)
|
|
6139
6239
|
state.layer.textContent = '';
|
|
6140
6240
|
}
|
|
6141
6241
|
/**
|
|
6142
6242
|
* One tab per component in the chain, pinned to the component's top-left
|
|
6143
|
-
* corner
|
|
6144
|
-
*
|
|
6145
|
-
*
|
|
6243
|
+
* corner. Width: the component's, but a narrow component still gets up to
|
|
6244
|
+
* LABEL_MIN_WIDTH so its name and args stay legible; a label that would then
|
|
6245
|
+
* run off the right edge is slid back so its right edge sits at the margin.
|
|
6246
|
+
* Outermost first so the innermost paints on top; tabs that share a corner (a
|
|
6247
|
+
* child filling its parent, the usual case) stack downward instead of hiding
|
|
6248
|
+
* each other.
|
|
6146
6249
|
*/
|
|
6147
6250
|
function render_labels() {
|
|
6148
6251
|
const layer = state.layer;
|
|
6149
6252
|
layer.textContent = '';
|
|
6253
|
+
state.label_hovered = false; // the element the pointer was on is gone
|
|
6150
6254
|
const taken = new Map();
|
|
6151
6255
|
for (let depth = state.chain.length - 1; depth >= 0; depth--) {
|
|
6152
6256
|
const component = state.chain[depth];
|
|
@@ -6159,14 +6263,33 @@ function render_labels() {
|
|
|
6159
6263
|
const stacked = taken.get(key) || 0;
|
|
6160
6264
|
taken.set(key, stacked + 1);
|
|
6161
6265
|
const label = el('div', `label ${PREFIX}depth-${depth % DEPTH_COLORS}`);
|
|
6162
|
-
|
|
6266
|
+
// Laid out at the left edge FIRST, then moved. An absolutely positioned box
|
|
6267
|
+
// shrink-to-fits into the room its own `left` leaves, so measuring it at its
|
|
6268
|
+
// final position would report the squeezed width, and moving it would let it
|
|
6269
|
+
// re-expand - the two chase each other. At left 0 it has the whole viewport,
|
|
6270
|
+
// so the measurement below is the width it will actually keep.
|
|
6271
|
+
label.style.left = '0px';
|
|
6163
6272
|
label.style.top = `${top + stacked * LABEL_HEIGHT}px`;
|
|
6164
|
-
label.style.maxWidth = `${
|
|
6273
|
+
label.style.maxWidth = `${rect.width < LABEL_MIN_WIDTH ? LABEL_MIN_WIDTH : Math.round(rect.width)}px`;
|
|
6165
6274
|
label.appendChild(el('span', 'label-name', component.component_name()));
|
|
6166
6275
|
const args = simple_args(component);
|
|
6167
6276
|
if (args)
|
|
6168
6277
|
label.appendChild(el('span', 'label-args', args));
|
|
6278
|
+
// Each label knows its own component: it is a hover target that holds the set
|
|
6279
|
+
// in place, and a click target that inspects THAT component, not the innermost.
|
|
6280
|
+
label.addEventListener('mouseenter', () => { state.label_hovered = true; });
|
|
6281
|
+
label.addEventListener('mouseleave', on_label_leave);
|
|
6282
|
+
label.addEventListener('click', (event) => {
|
|
6283
|
+
event.stopPropagation(); // the document handler already skips is_ours events
|
|
6284
|
+
select(component);
|
|
6285
|
+
});
|
|
6169
6286
|
layer.appendChild(label);
|
|
6287
|
+
// A label on a narrow component may be wider than the component, so a component
|
|
6288
|
+
// near the right edge can push its label off-screen. Slide it back until its
|
|
6289
|
+
// right edge sits on the margin, never past the left edge.
|
|
6290
|
+
const width = label.getBoundingClientRect().width || label.offsetWidth;
|
|
6291
|
+
const limit = window.innerWidth - VIEWPORT_MARGIN;
|
|
6292
|
+
label.style.left = `${left + width > limit ? Math.max(0, Math.round(limit - width)) : left}px`;
|
|
6170
6293
|
}
|
|
6171
6294
|
}
|
|
6172
6295
|
/** `$id=7 $title="Users"` - only strings, numbers and booleans, never internals. */
|
|
@@ -6196,7 +6319,7 @@ function on_click(event) {
|
|
|
6196
6319
|
close_modal();
|
|
6197
6320
|
return;
|
|
6198
6321
|
}
|
|
6199
|
-
|
|
6322
|
+
select(chain[0]);
|
|
6200
6323
|
}
|
|
6201
6324
|
function on_keydown(event) {
|
|
6202
6325
|
if (event.key === 'Escape' && state.modal_component) {
|
|
@@ -6210,17 +6333,82 @@ function close_modal() {
|
|
|
6210
6333
|
state.modal.classList.remove(`${PREFIX}modal-open`);
|
|
6211
6334
|
state.modal.textContent = '';
|
|
6212
6335
|
state.modal_component = null;
|
|
6336
|
+
state.nav_stack = [];
|
|
6337
|
+
set_selected(null);
|
|
6338
|
+
}
|
|
6339
|
+
// ---------------------------------------------------------------------------
|
|
6340
|
+
// Selection and navigation
|
|
6341
|
+
//
|
|
6342
|
+
// Three ways in, and they differ only in what they do to the navigation stack:
|
|
6343
|
+
// select() - a FRESH selection (page click, label click, inspect()): the stack
|
|
6344
|
+
// is dropped, so Back disappears.
|
|
6345
|
+
// navigate() - a step along the chain (Parent, an Ancestry entry, the
|
|
6346
|
+
// Instantiator link): the component being left is pushed.
|
|
6347
|
+
// show() - render only, stack untouched; what Back and the lifecycle
|
|
6348
|
+
// buttons re-open with.
|
|
6349
|
+
// ---------------------------------------------------------------------------
|
|
6350
|
+
function select(component) {
|
|
6351
|
+
state.nav_stack = [];
|
|
6352
|
+
show(component);
|
|
6353
|
+
}
|
|
6354
|
+
function navigate(component) {
|
|
6355
|
+
if (state.modal_component && state.modal_component !== component) {
|
|
6356
|
+
state.nav_stack.push(state.modal_component);
|
|
6357
|
+
}
|
|
6358
|
+
show(component);
|
|
6213
6359
|
}
|
|
6214
|
-
|
|
6360
|
+
/** Pop to the last component still alive; a stopped one is skipped, not shown. */
|
|
6361
|
+
function go_back() {
|
|
6362
|
+
while (state.nav_stack.length) {
|
|
6363
|
+
const previous = state.nav_stack.pop();
|
|
6364
|
+
if (previous.$.hasClass('_Component_Stopped'))
|
|
6365
|
+
continue;
|
|
6366
|
+
show(previous);
|
|
6367
|
+
return;
|
|
6368
|
+
}
|
|
6369
|
+
if (state.modal_component)
|
|
6370
|
+
show(state.modal_component); // nothing left: drop Back
|
|
6371
|
+
}
|
|
6372
|
+
/** Move SELECTED_CLASS to `element`, or take it off the page entirely for null. */
|
|
6373
|
+
function set_selected(element) {
|
|
6374
|
+
if (state.selected_element && state.selected_element !== element) {
|
|
6375
|
+
state.selected_element.classList.remove(SELECTED_CLASS);
|
|
6376
|
+
}
|
|
6377
|
+
state.selected_element = element;
|
|
6378
|
+
if (element)
|
|
6379
|
+
element.classList.add(SELECTED_CLASS);
|
|
6380
|
+
}
|
|
6381
|
+
/**
|
|
6382
|
+
* The inspector. Three children: a fixed title bar, a scrolling body holding every
|
|
6383
|
+
* section, and a fixed footer of lifecycle buttons. The modal sits top-right, but
|
|
6384
|
+
* moves to the left when the inspected component is itself in the right half of the
|
|
6385
|
+
* viewport - otherwise the panel covers the thing being inspected.
|
|
6386
|
+
*/
|
|
6387
|
+
function show(component) {
|
|
6215
6388
|
const modal = state.modal;
|
|
6216
6389
|
modal.textContent = '';
|
|
6217
6390
|
state.modal_component = component;
|
|
6218
6391
|
const chain = chain_for(component.$[0]);
|
|
6219
6392
|
const ancestors = chain.slice(1);
|
|
6220
6393
|
const element = component.$[0];
|
|
6221
|
-
|
|
6394
|
+
set_selected(element);
|
|
6395
|
+
modal.classList.toggle(`${PREFIX}modal-left`, element.getBoundingClientRect().left > window.innerWidth / 2);
|
|
6396
|
+
// Title bar: the name, then [Back] [Parent] [Log to console] [Close]. Back is
|
|
6397
|
+
// there only while something was navigated away from, Parent only while the
|
|
6398
|
+
// component has a DOM parent component - the first Ancestry entry.
|
|
6222
6399
|
const title = el('div', 'title');
|
|
6223
6400
|
title.appendChild(el('span', 'title-name', `<${component.component_name()}>`));
|
|
6401
|
+
if (state.nav_stack.length) {
|
|
6402
|
+
const back = el('button', 'button', 'Back');
|
|
6403
|
+
back.addEventListener('click', go_back);
|
|
6404
|
+
title.appendChild(back);
|
|
6405
|
+
}
|
|
6406
|
+
const parent = ancestors[0];
|
|
6407
|
+
if (parent) {
|
|
6408
|
+
const up = el('button', 'button', 'Parent');
|
|
6409
|
+
up.addEventListener('click', () => navigate(parent));
|
|
6410
|
+
title.appendChild(up);
|
|
6411
|
+
}
|
|
6224
6412
|
const log = el('button', 'button', 'Log to console');
|
|
6225
6413
|
log.addEventListener('click', () => {
|
|
6226
6414
|
console.log(`[JQHTML debug] <${component.component_name()}>`, component, element);
|
|
@@ -6230,6 +6418,9 @@ function open_modal(component) {
|
|
|
6230
6418
|
close.addEventListener('click', close_modal);
|
|
6231
6419
|
title.appendChild(close);
|
|
6232
6420
|
modal.appendChild(title);
|
|
6421
|
+
// The only scrolling element: the title bar and the footer stay put.
|
|
6422
|
+
const body = el('div', 'body');
|
|
6423
|
+
modal.appendChild(body);
|
|
6233
6424
|
// Identity
|
|
6234
6425
|
const ctor = component.constructor;
|
|
6235
6426
|
const hierarchy = ctor.get_class_hierarchy ? ctor.get_class_hierarchy() : [ctor.name];
|
|
@@ -6238,15 +6429,18 @@ function open_modal(component) {
|
|
|
6238
6429
|
row(identity, 'class', '', hierarchy.join(' → '));
|
|
6239
6430
|
row(identity, 'element', '', describe_element(element));
|
|
6240
6431
|
row(identity, '_cid', '', String(component._cid));
|
|
6241
|
-
|
|
6432
|
+
const lifecycle = READY_STATES[component._ready_state] || String(component._ready_state);
|
|
6433
|
+
row(identity, 'lifecycle', '', component._debug_no_data
|
|
6434
|
+
? `${lifecycle} (no data - reload w/o data)`
|
|
6435
|
+
: lifecycle);
|
|
6242
6436
|
const nocache = element.getAttribute('data-nocache');
|
|
6243
6437
|
if (nocache)
|
|
6244
6438
|
row(identity, 'cache', '', `declined: ${nocache}`);
|
|
6245
|
-
|
|
6439
|
+
body.appendChild(section('Identity', identity));
|
|
6246
6440
|
// Args / data / state
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6441
|
+
body.appendChild(section('Args', object_table(component.args)));
|
|
6442
|
+
body.appendChild(section('Data', object_table(component.data)));
|
|
6443
|
+
body.appendChild(section('State', object_table(component.state)));
|
|
6250
6444
|
// Ancestry
|
|
6251
6445
|
const list = el('div', 'list');
|
|
6252
6446
|
if (!ancestors.length)
|
|
@@ -6255,10 +6449,10 @@ function open_modal(component) {
|
|
|
6255
6449
|
const link = el('div', 'link');
|
|
6256
6450
|
link.appendChild(el('span', 'link-name', `<${ancestor.component_name()}>`));
|
|
6257
6451
|
link.appendChild(el('span', 'link-cid', String(ancestor._cid)));
|
|
6258
|
-
link.addEventListener('click', () =>
|
|
6452
|
+
link.addEventListener('click', () => navigate(ancestor));
|
|
6259
6453
|
list.appendChild(link);
|
|
6260
6454
|
}
|
|
6261
|
-
|
|
6455
|
+
body.appendChild(section('Ancestry (DOM, nearest first)', list));
|
|
6262
6456
|
// Instantiator - the component whose template wrote this tag. Differs from
|
|
6263
6457
|
// the DOM parent for markup written in a slot body or default content.
|
|
6264
6458
|
const instantiator = component.instantiator ? component.instantiator() : null;
|
|
@@ -6267,7 +6461,7 @@ function open_modal(component) {
|
|
|
6267
6461
|
const link = el('div', 'link');
|
|
6268
6462
|
link.appendChild(el('span', 'link-name', `<${instantiator.component_name()}>`));
|
|
6269
6463
|
link.appendChild(el('span', 'link-cid', String(instantiator._cid)));
|
|
6270
|
-
link.addEventListener('click', () =>
|
|
6464
|
+
link.addEventListener('click', () => navigate(instantiator));
|
|
6271
6465
|
inst.appendChild(link);
|
|
6272
6466
|
if (ancestors[0] && instantiator !== ancestors[0]) {
|
|
6273
6467
|
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.'));
|
|
@@ -6276,9 +6470,49 @@ function open_modal(component) {
|
|
|
6276
6470
|
else {
|
|
6277
6471
|
inst.appendChild(el('div', 'empty', 'none - created directly'));
|
|
6278
6472
|
}
|
|
6279
|
-
|
|
6473
|
+
body.appendChild(section('Instantiator', inst));
|
|
6474
|
+
// Footer: lifecycle controls. Clicks land inside the shadow host, so the
|
|
6475
|
+
// overlay's own click hijack lets them through (is_ours).
|
|
6476
|
+
const footer = el('div', 'footer');
|
|
6477
|
+
footer.appendChild(el('span', 'footer-label', 'Lifecycle:'));
|
|
6478
|
+
footer.appendChild(lifecycle_button(component, 'Reload', (c) => c.reload()));
|
|
6479
|
+
footer.appendChild(lifecycle_button(component, 'Refresh', (c) => c.refresh()));
|
|
6480
|
+
footer.appendChild(lifecycle_button(component, 'Rerender', (c) => c.render()));
|
|
6481
|
+
footer.appendChild(lifecycle_button(component, 'Reload w/o data', (c) => c._reload_without_data()));
|
|
6482
|
+
modal.appendChild(footer);
|
|
6280
6483
|
modal.classList.add(`${PREFIX}modal-open`);
|
|
6281
6484
|
}
|
|
6485
|
+
/**
|
|
6486
|
+
* One footer button. Runs its lifecycle call on a live component, logs whatever it
|
|
6487
|
+
* throws or rejects with, and re-opens the modal once the call settles so the panel
|
|
6488
|
+
* (lifecycle row above all) describes the state the button just produced.
|
|
6489
|
+
*/
|
|
6490
|
+
function lifecycle_button(component, label, run) {
|
|
6491
|
+
const button = el('button', 'button footer-button', label);
|
|
6492
|
+
button.addEventListener('click', () => {
|
|
6493
|
+
if (component.$.hasClass('_Component_Stopped'))
|
|
6494
|
+
return; // dead component: nothing to do
|
|
6495
|
+
let result;
|
|
6496
|
+
try {
|
|
6497
|
+
result = run(component);
|
|
6498
|
+
}
|
|
6499
|
+
catch (error) {
|
|
6500
|
+
console.error(`[JQHTML debug] ${label} threw on <${component.component_name()}>`, error);
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6503
|
+
Promise.resolve(result)
|
|
6504
|
+
.catch((error) => {
|
|
6505
|
+
console.error(`[JQHTML debug] ${label} rejected on <${component.component_name()}>`, error);
|
|
6506
|
+
})
|
|
6507
|
+
.then(() => {
|
|
6508
|
+
// `show`, not `select`: the lifecycle buttons re-render the panel in place
|
|
6509
|
+
// and must not throw away the navigation stack behind it.
|
|
6510
|
+
if (state.enabled && state.modal_component === component)
|
|
6511
|
+
show(component);
|
|
6512
|
+
});
|
|
6513
|
+
});
|
|
6514
|
+
return button;
|
|
6515
|
+
}
|
|
6282
6516
|
// ---------------------------------------------------------------------------
|
|
6283
6517
|
// DOM helpers (shadow side)
|
|
6284
6518
|
// ---------------------------------------------------------------------------
|
|
@@ -7009,7 +7243,7 @@ function init(jQuery, config) {
|
|
|
7009
7243
|
}
|
|
7010
7244
|
}
|
|
7011
7245
|
// Version - will be replaced during build with actual version from package.json
|
|
7012
|
-
const version = '2.3.
|
|
7246
|
+
const version = '2.3.70';
|
|
7013
7247
|
// Default export with all functionality
|
|
7014
7248
|
const jqhtml = {
|
|
7015
7249
|
// Core
|