@lowdefy/engine 0.0.0-experimental-20260929101721 → 0.0.0-experimental-20260929135001

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/Block.js CHANGED
@@ -507,10 +507,11 @@ let Block = class Block {
507
507
  toDelete,
508
508
  toSet
509
509
  }));
510
- return;
510
+ } else {
511
+ this.context._internal.State.republish(this.blockId, type.enforceType(this.meta.valueType, null));
511
512
  }
512
- this.context._internal.State.republish(this.blockId, type.enforceType(this.meta.valueType, null));
513
513
  }
514
+ // After the slots, so an input-container (an input with slots) republishes its own value.
514
515
  if (this.isInput()) {
515
516
  this.context._internal.State.republish(this.blockId, this.value);
516
517
  }
package/dist/Events.js CHANGED
@@ -31,8 +31,13 @@ let Events = class Events {
31
31
  this.events[eventName] = this.initEvent(this.block.events[eventName]);
32
32
  });
33
33
  }
34
+ // Events a block registers for its own machinery (Upload's policy request, a Table's row
35
+ // fetch). They are internal: see claimDomEvent.
34
36
  registerEvent({ name, actions }) {
35
- this.events[name] = this.initEvent(actions);
37
+ this.events[name] = {
38
+ ...this.initEvent(actions),
39
+ internal: true
40
+ };
36
41
  }
37
42
  triggerEvent({ name, event, progress }) {
38
43
  this.context._internal.lowdefy.eventCallback?.({
@@ -55,7 +60,8 @@ let Events = class Events {
55
60
  const handledBy = claimDomEvent({
56
61
  blockId: this.block.blockId,
57
62
  bubble: eventDescription?.bubble === true,
58
- hasActions: !type.isUndefined(eventDescription)
63
+ hasActions: !type.isUndefined(eventDescription),
64
+ internal: eventDescription?.internal === true
59
65
  });
60
66
  if (!type.isNull(handledBy)) {
61
67
  result.handledBy = handledBy;
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { applyArrayIndices, type } from '@lowdefy/helpers';
16
+ import exemptFromDomEvent from '../exemptFromDomEvent.js';
16
17
  function createCallMethod({ arrayIndices, context }) {
17
18
  return function callMethod(params) {
18
19
  const { blockId, method, args = [] } = params;
@@ -29,6 +30,9 @@ function createCallMethod({ arrayIndices, context }) {
29
30
  if (!type.isFunction(blockMethod)) {
30
31
  throw new Error(`Failed to call method "${method}" on block "${blockId}". Check if "${method}" is a valid block method for block "${blockId}".`);
31
32
  }
33
+ exemptFromDomEvent({
34
+ blockId: block.blockId
35
+ });
32
36
  return blockMethod(...args);
33
37
  };
34
38
  }
@@ -12,21 +12,48 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ // DOM event -> id of the block whose event handled it. Weak, so dispatched events are collected.
16
- const domEventHandlers = new WeakMap();
17
- // A DOM event bubbles through every block that wraps its target, and each of those blocks may
18
- // fire an event for it: a Button click also reaches the clickable Card around it. The first
19
- // block with actions for the event handles it, and other blocks skip the same DOM event unless
20
- // the handling event sets `bubble: true`. window.event is the DOM event whose listeners are
21
- // running (React dispatches synchronously inside it), and undefined outside a DOM dispatch, so
22
- // events fired by actions, timers or requests are never affected.
23
- function claimDomEvent({ blockId, bubble, hasActions }) {
24
- const domEvent = globalThis.window?.event;
25
- if (!(domEvent instanceof globalThis.Event)) return null;
26
- const handledBy = domEventHandlers.get(domEvent);
27
- if (handledBy !== undefined && handledBy !== blockId) return handledBy;
15
+ */ import domEventClaims from './domEventClaims.js';
16
+ import getDomEvent from './getDomEvent.js';
17
+ import getPathIndex from './getPathIndex.js';
18
+ // A DOM event bubbles through every block that wraps its target, and each of those blocks may fire
19
+ // an event for it: a Button click also reaches the clickable Card around it. The innermost block on
20
+ // the event's path with actions for the event handles it, and the blocks further out on the path
21
+ // skip their events for the same DOM event, unless the handling event sets `bubble: true`.
22
+ //
23
+ // Only blocks the DOM event passed through take part. Blocks fire events for other reasons while
24
+ // a DOM event is dispatched, and those are never skipped:
25
+ // - A block off the path: a Table next to a Button fires onSelectionChange from the effect that
26
+ // commits the selection the Button's CallMethod cleared. React flushes that effect in a
27
+ // microtask inside the click listener, so window.event is still the click. The same holds for
28
+ // effects in React's scheduler `message` events, whose path has no blocks.
29
+ // - A block inside the handling block: a Table in a clickable Card fires onSelectionChange for a
30
+ // checkbox after the Card's onClick ran. It runs, and holds the claim from then on.
31
+ // - A block an action called a method on (exemptFromDomEvent), even when it is on the path: a
32
+ // Button in a Table's bulk action slot clearing the Table's selection.
33
+ // - An internal event (registered by the block itself, like a Table's row fetch). It still claims
34
+ // an unclaimed DOM event, as the block's own handler would.
35
+ function claimDomEvent({ blockId, bubble, hasActions, internal }) {
36
+ const domEvent = getDomEvent();
37
+ if (domEvent === null) return null;
38
+ const path = domEvent.composedPath();
39
+ const index = getPathIndex({
40
+ blockId,
41
+ path
42
+ });
43
+ if (index === -1) return null;
44
+ if (domEventClaims.exempt.get(domEvent)?.has(blockId)) return null;
45
+ const handledBy = domEventClaims.handledBy.get(domEvent);
46
+ if (handledBy !== undefined && handledBy !== blockId) {
47
+ const handledByIsInner = getPathIndex({
48
+ blockId: handledBy,
49
+ path
50
+ }) < index;
51
+ if (handledByIsInner) {
52
+ return internal === true ? null : handledBy;
53
+ }
54
+ }
28
55
  if (hasActions && !bubble) {
29
- domEventHandlers.set(domEvent, blockId);
56
+ domEventClaims.handledBy.set(domEvent, blockId);
30
57
  }
31
58
  return null;
32
59
  }
@@ -0,0 +1,151 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { serializer } from '@lowdefy/helpers';
16
+ import { WebParser } from '@lowdefy/operators';
17
+ import Actions from './Actions.js';
18
+ import DependencyTracker from './tracking/DependencyTracker.js';
19
+ import readMediaViewport from './tracking/readMediaViewport.js';
20
+ import Slots from './Slots.js';
21
+ import Requests from './Requests.js';
22
+ import State from './State.js';
23
+ import updateMedia from './tracking/updateMedia.js';
24
+ import WebSockets from './WebSockets.js';
25
+ const blockData = (config)=>{
26
+ const { slots, blockId, blocks, class: blockClass, events, field, id, layout, loading, pageId, properties, requests, required, skeleton, style, subscriptions, type, validate, visible } = config;
27
+ const result = {
28
+ slots,
29
+ blockId,
30
+ blocks,
31
+ class: blockClass,
32
+ events,
33
+ field,
34
+ id,
35
+ layout,
36
+ loading,
37
+ pageId,
38
+ properties,
39
+ requests,
40
+ required,
41
+ skeleton,
42
+ style,
43
+ subscriptions,
44
+ type,
45
+ validate,
46
+ visible
47
+ };
48
+ // Preserve ~k (configKey) for error tracing - it's non-enumerable so must be copied explicitly
49
+ if (config['~k']) {
50
+ Object.defineProperty(result, '~k', {
51
+ value: config['~k'],
52
+ enumerable: false,
53
+ writable: true,
54
+ configurable: true
55
+ });
56
+ }
57
+ return result;
58
+ };
59
+ // Builds an engine context around one root block: the page's root block for a
60
+ // page context, and a block-less root carrying the app events for the app context.
61
+ function createContext({ config, jsMap, lowdefy }) {
62
+ const { id } = config;
63
+ const ctx = {
64
+ id,
65
+ pageId: config.pageId,
66
+ eventLog: [],
67
+ jsMap,
68
+ requests: {},
69
+ state: {},
70
+ _internal: {
71
+ lowdefy,
72
+ // Config object reference for dynamic page memoization — identity marks
73
+ // which fetch this context was built from.
74
+ pageConfig: config,
75
+ // The viewport the page's blocks last evaluated _media against, or null without a window.
76
+ // updateMedia diffs a resize against it.
77
+ media: readMediaViewport({
78
+ lowdefy
79
+ }),
80
+ // The read recorder of the block evaluating itself, or null. Managed by DependencyTracker.
81
+ readRecorder: null,
82
+ rootBlock: blockData(config),
83
+ update: ()=>{},
84
+ // React updaters register here per block id when the context's Block
85
+ // components mount — scoped per context so rebuilding over a live
86
+ // context (dynamic page navigation, reset) never notifies the previous
87
+ // context's still-mounted components.
88
+ updaters: {}
89
+ }
90
+ };
91
+ const _internal = ctx._internal;
92
+ _internal.DependencyTracker = new DependencyTracker(ctx);
93
+ _internal.parser = new WebParser({
94
+ context: ctx,
95
+ operators: lowdefy._internal.operators
96
+ });
97
+ _internal.State = new State(ctx);
98
+ _internal.Actions = new Actions(ctx);
99
+ _internal.Requests = new Requests(ctx);
100
+ _internal.WebSockets = new WebSockets(ctx);
101
+ _internal.RootSlots = new Slots({
102
+ slots: {
103
+ root: {
104
+ blocks: [
105
+ _internal.rootBlock
106
+ ]
107
+ }
108
+ },
109
+ context: ctx
110
+ });
111
+ _internal.RootSlots.init(serializer.copy(ctx.state));
112
+ // update({ changes }) is a dependency-tracked pass; a bare update() is a full pass.
113
+ _internal.update = (options)=>{
114
+ _internal.RootSlots.update(options);
115
+ };
116
+ _internal.runOnInit = async (progress)=>{
117
+ progress();
118
+ if (!_internal.onInitDone) {
119
+ await _internal.RootSlots.slots.root.blocks[0].triggerEvent({
120
+ name: 'onInit',
121
+ progress
122
+ });
123
+ _internal.update();
124
+ _internal.State.freezeState();
125
+ _internal.onInitDone = true;
126
+ }
127
+ };
128
+ _internal.runOnInitAsync = async (progress)=>{
129
+ if (_internal.onInitDone && !_internal.onInitAsyncDone) {
130
+ await _internal.RootSlots.slots.root.blocks[0].triggerEvent({
131
+ name: 'onInitAsync',
132
+ progress
133
+ });
134
+ _internal.onInitAsyncDone = true;
135
+ }
136
+ };
137
+ // Called by the client's debounced resize listener.
138
+ _internal.updateMedia = ()=>updateMedia({
139
+ context: ctx
140
+ });
141
+ // Page lifecycle events (onVisible, onHidden, onOnline, onOffline, onResize) are
142
+ // triggered on the page's root block by browser listeners attached in the client.
143
+ _internal.triggerPageEvent = ({ name, event, progress = ()=>undefined })=>_internal.RootSlots.slots.root.blocks[0].triggerEvent({
144
+ name,
145
+ event,
146
+ progress
147
+ });
148
+ ctx._internal.update();
149
+ return ctx;
150
+ }
151
+ export default createContext;
@@ -0,0 +1,22 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // Per DOM event, weak so dispatched events are collected:
16
+ // - handledBy: the id of the block whose event handled it.
17
+ // - exempt: ids of blocks an action called a method on while it was dispatched.
18
+ const domEventClaims = {
19
+ handledBy: new WeakMap(),
20
+ exempt: new WeakMap()
21
+ };
22
+ export default domEventClaims;
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import domEventClaims from './domEventClaims.js';
16
+ import getDomEvent from './getDomEvent.js';
17
+ // A block an action calls a method on acts for that action, not for the DOM event being
18
+ // dispatched: the events it fires for it (at once, or from the effects the method causes) are
19
+ // never skipped as copies of the DOM event bubbling through it. See claimDomEvent.
20
+ function exemptFromDomEvent({ blockId }) {
21
+ const domEvent = getDomEvent();
22
+ if (domEvent === null) return;
23
+ const exempt = domEventClaims.exempt.get(domEvent) ?? new Set();
24
+ exempt.add(blockId);
25
+ domEventClaims.exempt.set(domEvent, exempt);
26
+ }
27
+ export default exemptFromDomEvent;
@@ -0,0 +1,66 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import createContext from './createContext.js';
16
+ // App events are not page config: the app context runs them on a root without
17
+ // blocks, so they have no page state, inputs or requests. Box is the root because
18
+ // every app bundles it (a container, so it has no value in state).
19
+ function getAppContext({ events, jsMap = {}, lowdefy }) {
20
+ if (lowdefy.appContext) {
21
+ return lowdefy.appContext;
22
+ }
23
+ const ctx = createContext({
24
+ config: {
25
+ id: 'app',
26
+ blockId: 'app',
27
+ type: 'Box',
28
+ events
29
+ },
30
+ jsMap,
31
+ lowdefy
32
+ });
33
+ const _internal = ctx._internal;
34
+ // Global and API responses are app-wide: a change an app event makes must
35
+ // re-render the pages, and reportAppChange has already reported it to them.
36
+ const updateAppRoot = _internal.update;
37
+ _internal.update = (options)=>{
38
+ updateAppRoot(options);
39
+ Object.values(lowdefy.contexts).forEach((pageContext)=>{
40
+ pageContext._internal.update({
41
+ changes: []
42
+ });
43
+ });
44
+ };
45
+ // Every page mount asks for the app events, and a navigation can mount the
46
+ // next page before they finish, so each runs once and later callers share it.
47
+ const runOnInit = _internal.runOnInit;
48
+ const runOnInitAsync = _internal.runOnInitAsync;
49
+ let onInitPromise = null;
50
+ let onInitAsyncPromise = null;
51
+ _internal.runOnInit = (progress)=>{
52
+ if (!onInitPromise) {
53
+ onInitPromise = runOnInit(progress);
54
+ }
55
+ return onInitPromise;
56
+ };
57
+ _internal.runOnInitAsync = (progress)=>{
58
+ if (!onInitAsyncPromise) {
59
+ onInitAsyncPromise = _internal.runOnInit(progress).then(()=>runOnInitAsync(progress));
60
+ }
61
+ return onInitAsyncPromise;
62
+ };
63
+ lowdefy.appContext = ctx;
64
+ return ctx;
65
+ }
66
+ export default getAppContext;
@@ -13,50 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { LowdefyInternalError } from '@lowdefy/errors';
16
- import { serializer } from '@lowdefy/helpers';
17
- import { WebParser } from '@lowdefy/operators';
18
- import Actions from './Actions.js';
19
- import DependencyTracker from './tracking/DependencyTracker.js';
20
- import readMediaViewport from './tracking/readMediaViewport.js';
21
- import Slots from './Slots.js';
22
- import Requests from './Requests.js';
23
- import State from './State.js';
24
- import updateMedia from './tracking/updateMedia.js';
25
- import WebSockets from './WebSockets.js';
26
- const blockData = (config)=>{
27
- const { slots, blockId, blocks, class: blockClass, events, field, id, layout, loading, pageId, properties, requests, required, skeleton, style, subscriptions, type, validate, visible } = config;
28
- const result = {
29
- slots,
30
- blockId,
31
- blocks,
32
- class: blockClass,
33
- events,
34
- field,
35
- id,
36
- layout,
37
- loading,
38
- pageId,
39
- properties,
40
- requests,
41
- required,
42
- skeleton,
43
- style,
44
- subscriptions,
45
- type,
46
- validate,
47
- visible
48
- };
49
- // Preserve ~k (configKey) for error tracing - it's non-enumerable so must be copied explicitly
50
- if (config['~k']) {
51
- Object.defineProperty(result, '~k', {
52
- value: config['~k'],
53
- enumerable: false,
54
- writable: true,
55
- configurable: true
56
- });
57
- }
58
- return result;
59
- };
16
+ import createContext from './createContext.js';
60
17
  function getContext({ config, jsMap = {}, lowdefy, resetContext = {
61
18
  reset: false,
62
19
  setReset: ()=>undefined
@@ -75,10 +32,6 @@ function getContext({ config, jsMap = {}, lowdefy, resetContext = {
75
32
  // memoize context if already created, eg between page transitions, unless the reset flag is
76
33
  // raised. A full pass: this render-time update is how changes with no reporting hook reach the
77
34
  // page - user, i18n, theme, menus, inputs and the URL.
78
- // The pass reads the current viewport, which a resize while the page was away changed.
79
- lowdefy.contexts[id]._internal.media = readMediaViewport({
80
- lowdefy
81
- });
82
35
  lowdefy.contexts[id]._internal.update();
83
36
  return lowdefy.contexts[id];
84
37
  }
@@ -92,92 +45,11 @@ function getContext({ config, jsMap = {}, lowdefy, resetContext = {
92
45
  if (!lowdefy.inputs[id]) {
93
46
  lowdefy.inputs[id] = {};
94
47
  }
95
- const ctx = {
96
- id,
97
- pageId: config.pageId,
98
- eventLog: [],
48
+ const ctx = createContext({
49
+ config,
99
50
  jsMap,
100
- requests: {},
101
- state: {},
102
- _internal: {
103
- lowdefy,
104
- // Config object reference for dynamic page memoization — identity marks
105
- // which fetch this context was built from.
106
- pageConfig: config,
107
- // The viewport the page's blocks last evaluated _media against, or null without a window.
108
- // updateMedia diffs a resize against it.
109
- media: readMediaViewport({
110
- lowdefy
111
- }),
112
- // The read recorder of the block evaluating itself, or null. Managed by DependencyTracker.
113
- readRecorder: null,
114
- rootBlock: blockData(config),
115
- update: ()=>{},
116
- // React updaters register here per block id when the context's Block
117
- // components mount — scoped per context so rebuilding over a live
118
- // context (dynamic page navigation, reset) never notifies the previous
119
- // context's still-mounted components.
120
- updaters: {}
121
- }
122
- };
123
- const _internal = ctx._internal;
124
- _internal.DependencyTracker = new DependencyTracker(ctx);
125
- _internal.parser = new WebParser({
126
- context: ctx,
127
- operators: lowdefy._internal.operators
128
- });
129
- _internal.State = new State(ctx);
130
- _internal.Actions = new Actions(ctx);
131
- _internal.Requests = new Requests(ctx);
132
- _internal.WebSockets = new WebSockets(ctx);
133
- _internal.RootSlots = new Slots({
134
- slots: {
135
- root: {
136
- blocks: [
137
- _internal.rootBlock
138
- ]
139
- }
140
- },
141
- context: ctx
51
+ lowdefy
142
52
  });
143
- _internal.RootSlots.init(serializer.copy(ctx.state));
144
- // update({ changes }) is a dependency-tracked pass; a bare update() is a full pass.
145
- _internal.update = (options)=>{
146
- _internal.RootSlots.update(options);
147
- };
148
- _internal.runOnInit = async (progress)=>{
149
- progress();
150
- if (!_internal.onInitDone) {
151
- await _internal.RootSlots.slots.root.blocks[0].triggerEvent({
152
- name: 'onInit',
153
- progress
154
- });
155
- _internal.update();
156
- _internal.State.freezeState();
157
- _internal.onInitDone = true;
158
- }
159
- };
160
- _internal.runOnInitAsync = async (progress)=>{
161
- if (_internal.onInitDone && !_internal.onInitAsyncDone) {
162
- await _internal.RootSlots.slots.root.blocks[0].triggerEvent({
163
- name: 'onInitAsync',
164
- progress
165
- });
166
- _internal.onInitAsyncDone = true;
167
- }
168
- };
169
- // Called by the client's debounced resize listener.
170
- _internal.updateMedia = ()=>updateMedia({
171
- context: ctx
172
- });
173
- // Page lifecycle events (onVisible, onHidden, onOnline, onOffline, onResize) are
174
- // triggered on the page's root block by browser listeners attached in the client.
175
- _internal.triggerPageEvent = ({ name, event, progress = ()=>undefined })=>_internal.RootSlots.slots.root.blocks[0].triggerEvent({
176
- name,
177
- event,
178
- progress
179
- });
180
- ctx._internal.update();
181
53
  lowdefy.contexts[id] = ctx;
182
54
  return ctx;
183
55
  }
@@ -0,0 +1,25 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // The DOM event whose listeners are running: window.event is set while a listener runs, including
16
+ // the microtasks that run before the listener returns (React flushes discrete updates and their
17
+ // effects there), and undefined outside a DOM dispatch.
18
+ function getDomEvent() {
19
+ // The engine's unit tests run without a window.
20
+ if (typeof window === 'undefined') return null;
21
+ const domEvent = window.event;
22
+ if (!(domEvent instanceof Event)) return null;
23
+ return domEvent;
24
+ }
25
+ export default getDomEvent;
@@ -0,0 +1,23 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // The client renders every block inside a layout element with the id `bl-<blockId>`
16
+ // (packages/client/src/block). Its position on the DOM event's path, innermost first, or -1 when
17
+ // the event did not pass through the block.
18
+ function getPathIndex({ blockId, path }) {
19
+ const element = document.getElementById(`bl-${blockId}`);
20
+ if (element === null) return -1;
21
+ return path.indexOf(element);
22
+ }
23
+ export default getPathIndex;
package/dist/index.js CHANGED
@@ -17,11 +17,12 @@ import Slots from './Slots.js';
17
17
  import createLink from './createLink.js';
18
18
  import decodeServerError, { getDevError } from './decodeServerError.js';
19
19
  import Events from './Events.js';
20
+ import getAppContext from './getAppContext.js';
20
21
  import getContext from './getContext.js';
21
22
  import getHomePathname from './getHomePathname.js';
22
23
  import Requests from './Requests.js';
23
24
  import resolveTarget from './resolveTarget.js';
24
25
  import State from './State.js';
25
26
  import stopChain from './stopChain.js';
26
- export { Actions, Slots, createLink, decodeServerError, Events, getDevError, getHomePathname, Requests, resolveTarget, State, stopChain };
27
+ export { Actions, Slots, createLink, decodeServerError, Events, getAppContext, getDevError, getHomePathname, Requests, resolveTarget, State, stopChain };
27
28
  export default getContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/engine",
3
- "version": "0.0.0-experimental-20260929101721",
3
+ "version": "0.0.0-experimental-20260929135001",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -30,16 +30,16 @@
30
30
  "dist/*"
31
31
  ],
32
32
  "dependencies": {
33
- "@lowdefy/errors": "0.0.0-experimental-20260929101721",
34
- "@lowdefy/helpers": "0.0.0-experimental-20260929101721",
35
- "@lowdefy/operators": "0.0.0-experimental-20260929101721"
33
+ "@lowdefy/errors": "0.0.0-experimental-20260929135001",
34
+ "@lowdefy/helpers": "0.0.0-experimental-20260929135001",
35
+ "@lowdefy/operators": "0.0.0-experimental-20260929135001"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@jest/globals": "28.1.3",
39
- "@lowdefy/actions-core": "0.0.0-experimental-20260929101721",
40
- "@lowdefy/build": "0.0.0-experimental-20260929101721",
41
- "@lowdefy/operators-js": "0.0.0-experimental-20260929101721",
42
- "@lowdefy/operators-mql": "0.0.0-experimental-20260929101721",
39
+ "@lowdefy/actions-core": "0.0.0-experimental-20260929135001",
40
+ "@lowdefy/build": "0.0.0-experimental-20260929135001",
41
+ "@lowdefy/operators-js": "0.0.0-experimental-20260929135001",
42
+ "@lowdefy/operators-mql": "0.0.0-experimental-20260929135001",
43
43
  "@swc/cli": "0.8.1",
44
44
  "@swc/core": "1.15.32",
45
45
  "@swc/jest": "0.2.39",