From 6b27b21cc4b73924c6ecfe56af5ba90f5aa4cc97 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 12:47:14 +0400 Subject: [PATCH] feat(data): an own-rule selection owner keeps its whole old click handler Vessels' click rule (sibling ownership, cyclone background, card hit test, unkeyed records) does not map onto the plain or look-under rules: an owner with onClick(ctx) gets every click with the top pick, no gesture rule (its old LEFT_CLICK handler), and is another layer to the rest. --- src/data/selectionDispatcher.js | 19 ++++++++++-- src/data/selectionDispatcher.test.mjs | 44 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/data/selectionDispatcher.js b/src/data/selectionDispatcher.js index 0bf2a8dcd..a1cfa201a 100644 --- a/src/data/selectionDispatcher.js +++ b/src/data/selectionDispatcher.js @@ -20,6 +20,10 @@ // nothing, is empty space for it (it releases its selection); a click that // only meets what it looks under does nothing; no gesture rule (its old // handler took every click). To the others it is another layer. +// An own-rule owner (`onClick(ctx)`: vessels) keeps its whole old click +// handler: it gets every click with the top pick and decides itself (no +// gesture rule, as its old LEFT_CLICK handler); to the others it is another +// layer. import { isPointerFree as pointerFree } from './inputOwnership.js'; import { @@ -104,7 +108,9 @@ export function createSelectionDispatcher( } return lookedUnder ? 'under' : null; }; - const plain = (owner) => typeof owner.lookUnder !== 'function'; + const ownRule = (owner) => typeof owner.onClick === 'function'; + const plain = (owner) => + typeof owner.lookUnder !== 'function' && !ownRule(owner); const onClick = (event) => { if (press) finish(event.position); @@ -119,6 +125,15 @@ export function createSelectionDispatcher( }; for (const [name, owner] of [...owners]) { if (plain(owner)) continue; + if (ownRule(owner)) { + // Its old handler ran alone: a throw there leaves the others be. + try { + owner.onClick(ctx); + } catch (error) { + console.error(error); + } + continue; + } const target = lookUnderTarget(name, owner, event); if (target === 'under') continue; if (target && target !== 'other') @@ -205,7 +220,7 @@ export function createSelectionDispatcher( * @param {string} name the owning layer's id * @param {{ owns: (id: string) => boolean, onSelect?: Function, onEmptyClick?: Function, * onEscape?: Function, passThrough?: (picked: object) => boolean, drillThrough?: boolean, - * lookUnder?: (id: string) => boolean }} owner + * lookUnder?: (id: string) => boolean, onClick?: (ctx: object) => void }} owner * @returns {() => void} unregister */ register(name, owner) { diff --git a/src/data/selectionDispatcher.test.mjs b/src/data/selectionDispatcher.test.mjs index e1ad5ce0b..892e35f19 100644 --- a/src/data/selectionDispatcher.test.mjs +++ b/src/data/selectionDispatcher.test.mjs @@ -256,3 +256,47 @@ test('a look-under owner (local ADS-B) keeps its own click rule beside the other unregisterPickOwner('cctv'); } }); + +test('an own-rule owner (vessels) gets every click with its top pick and is another layer to the rest', () => { + const { dispatcher, click, escape } = setup({ + 10: [{ id: { mmsi: '111' } }], // its own object (Cesium: the record) + 20: [{ id: 'ae01ce' }], // flights' contact + 30: [], // empty map + }); + const seen = []; + const flights = recorder(); + let owned = true; + dispatcher.register('ais-live-vessels', { + owns: (id) => owned && id === '111', + onClick: (ctx) => + seen.push([ctx.position.x, ctx.picked?.id ?? null, ctx.gesture.travelPx]), + }); + dispatcher.register('flights', { + owns: (id) => id === 'ae01ce', + ...flights.handlers, + }); + click(10); + click(20); + click(30); + // No gesture rule: a long press and a drag still reach it (LEFT_CLICK). + click(30, { ms: 900 }); + click(30, { travel: 40 }); + escape(); + assert.deepEqual(seen, [ + [10, { mmsi: '111' }, 0], + [20, 'ae01ce', 0], + [30, null, 0], + [30, null, 0], + [70, null, 40], + ]); + // To flights a vessel is another layer's object: not its empty space. + assert.deepEqual(flights.calls, [ + ['select', 'ae01ce', undefined], + ['empty', 0], + ['escape'], + ]); + // A disowned vessel pick (the layer disabled) is empty space again. + owned = false; + click(10); + assert.deepEqual(flights.calls.at(-1), ['empty', 0]); +});