mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(globe): answer the clear and share actions through the globe actions store
The top-center bar is now GlobeActions.vue. The shell publishes whether a clear is under way and answers the clear and share buttons through the globe actions store, instead of binding listeners and writing attributes on the buttons. A shell disposed mid-clear resets the busy state, so the next shell's bar is not left busy. bindClearLayersControl stays exported and is deprecated.
This commit is contained in:
1 parent
f269f5be4f
commit
7edf8a577e
15 files changed
+221
-54
No files matched your search
+5
-3
@@ -72,9 +72,11 @@
|
|||||||
are the kit's new `UiChip`. The selects are the kit's new `UiSelect`, the
|
are the kit's new `UiChip`. The selects are the kit's new `UiSelect`, the
|
||||||
status badges its new `UiBadge` and the one-line empty states its new
|
status badges its new `UiBadge` and the one-line empty states its new
|
||||||
`UiEmpty`.
|
`UiEmpty`.
|
||||||
`syncScopeControls`
|
The top-center clear and share buttons call the shell through the new
|
||||||
(`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are
|
globe actions store. `syncScopeControls` (`./ui/display`),
|
||||||
deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue
|
`shouldHideCollapsedRightPanels` (`./ui/layout`) and
|
||||||
|
`bindClearLayersControl` (`./ui/layers`) are deprecated: the Display view,
|
||||||
|
the rail and the globe actions view now own what they did. `./ui/input` imports Vue
|
||||||
(samehkhamis).
|
(samehkhamis).
|
||||||
|
|
||||||
- `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers
|
- `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers
|
||||||
|
|||||||
@@ -225,7 +225,10 @@ a feature supplying only its accent.
|
|||||||
`splitFlap.js` plans, writing only the label's one text node; the toast
|
`splitFlap.js` plans, writing only the label's one text node; the toast
|
||||||
renders what each owner's `createToast()` publishes there, on that
|
renders what each owner's `createToast()` publishes there, on that
|
||||||
owner's own timer, and the title bar's FPS readout what
|
owner's own timer, and the title bar's FPS readout what
|
||||||
`createFrameRateMonitor()` publishes), the Scene actions panel (`SceneInteractions`
|
`createFrameRateMonitor()` publishes), the globe actions (in progress:
|
||||||
|
`GlobeActions` renders the top-center bar; its clear and share buttons
|
||||||
|
call the shell through the `gev-globe-actions` store, which also carries
|
||||||
|
a clear under way), the Scene actions panel (`SceneInteractions`
|
||||||
teleports what the scene interactions owner publishes to the
|
teleports what the scene interactions owner publishes to the
|
||||||
`gev-scene-interactions` store into the viewer's container; the owner
|
`gev-scene-interactions` store into the viewer's container; the owner
|
||||||
keeps the pick handler and focuses the picked action), the data-pack
|
keeps the pick handler and focuses the picked action), the data-pack
|
||||||
|
|||||||
@@ -1134,6 +1134,7 @@
|
|||||||
"src/ui/kit/UiButton.test.mjs",
|
"src/ui/kit/UiButton.test.mjs",
|
||||||
"src/ui/kit/UiChip.vue",
|
"src/ui/kit/UiChip.vue",
|
||||||
"src/ui/kit/UiChip.test.mjs",
|
"src/ui/kit/UiChip.test.mjs",
|
||||||
|
"src/ui/globeActions.test.mjs",
|
||||||
"src/ui/kit/UiSelect.js",
|
"src/ui/kit/UiSelect.js",
|
||||||
"src/ui/kit/UiSelect.test.mjs",
|
"src/ui/kit/UiSelect.test.mjs",
|
||||||
"src/ui/kit/UiBadge.vue",
|
"src/ui/kit/UiBadge.vue",
|
||||||
|
|||||||
@@ -138,7 +138,9 @@
|
|||||||
"src/ui/kit/UiChip.vue",
|
"src/ui/kit/UiChip.vue",
|
||||||
"src/ui/kit/UiSelect.js",
|
"src/ui/kit/UiSelect.js",
|
||||||
"src/ui/kit/UiEmpty.vue",
|
"src/ui/kit/UiEmpty.vue",
|
||||||
"src/ui/kit/UiBadge.vue"
|
"src/ui/kit/UiBadge.vue",
|
||||||
|
"src/ui/stores/globeActions.js",
|
||||||
|
"src/ui/views/panels/GlobeActions.vue"
|
||||||
],
|
],
|
||||||
"external": ["vue", "pinia"]
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -1076,7 +1078,8 @@
|
|||||||
"src/ui/rail/fieldValue.js",
|
"src/ui/rail/fieldValue.js",
|
||||||
"src/ui/kit/UiSlider.js",
|
"src/ui/kit/UiSlider.js",
|
||||||
"src/ui/kit/UiButton.vue",
|
"src/ui/kit/UiButton.vue",
|
||||||
"src/ui/kit/UiBadge.vue"
|
"src/ui/kit/UiBadge.vue",
|
||||||
|
"src/ui/stores/globeActions.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "vue", "pinia"]
|
"external": ["cesium", "vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -3089,7 +3092,8 @@
|
|||||||
"src/ui/kit/UiSlider.js",
|
"src/ui/kit/UiSlider.js",
|
||||||
"src/ui/kit/UiButton.vue",
|
"src/ui/kit/UiButton.vue",
|
||||||
"src/ui/kit/UiEmpty.vue",
|
"src/ui/kit/UiEmpty.vue",
|
||||||
"src/ui/kit/UiBadge.vue"
|
"src/ui/kit/UiBadge.vue",
|
||||||
|
"src/ui/stores/globeActions.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
|
|||||||
@@ -87,17 +87,21 @@ try {
|
|||||||
row().querySelector('.data-toggle-btn').dataset.feedState === 'stale',
|
row().querySelector('.data-toggle-btn').dataset.feedState === 'stale',
|
||||||
]);
|
]);
|
||||||
const ui = window.__godsEyeView.styleManager;
|
const ui = window.__godsEyeView.styleManager;
|
||||||
ui._clearSelectedLayersBtn.click();
|
const clearButton = document.getElementById('clear-selected-layers');
|
||||||
|
clearButton.click();
|
||||||
|
// The bar renders the busy state on Vue's next flush.
|
||||||
|
await Promise.resolve();
|
||||||
result.push([
|
result.push([
|
||||||
'native clear activation presents busy state',
|
'native clear activation presents busy state',
|
||||||
ui._clearSelectedLayersBtn.getAttribute('aria-busy') === 'true' &&
|
clearButton.getAttribute('aria-busy') === 'true' &&
|
||||||
!!ui._contextControls._clearSelectedLayersPromise,
|
!!ui._contextControls._clearSelectedLayersPromise,
|
||||||
]);
|
]);
|
||||||
await ui._contextControls._clearSelectedLayersPromise;
|
await ui._contextControls._clearSelectedLayersPromise;
|
||||||
|
await Promise.resolve();
|
||||||
result.push([
|
result.push([
|
||||||
'clear settles through the existing layer transaction',
|
'clear settles through the existing layer transaction',
|
||||||
!manager.isEnabled(id) &&
|
!manager.isEnabled(id) &&
|
||||||
ui._clearSelectedLayersBtn.getAttribute('aria-busy') === 'false',
|
clearButton.getAttribute('aria-busy') === 'false',
|
||||||
]);
|
]);
|
||||||
await manager.setEnabled(id, true);
|
await manager.setEnabled(id, true);
|
||||||
const final = row().querySelector('.data-toggle-btn');
|
const final = row().querySelector('.data-toggle-btn');
|
||||||
|
|||||||
@@ -87,11 +87,13 @@ test('Context async action buttons remain focused while busy', () => {
|
|||||||
assert.ok(enable, 'Radio Enable button is missing');
|
assert.ok(enable, 'Radio Enable button is missing');
|
||||||
assert.doesNotMatch(enable, /disabled(?!=")|:disabled/);
|
assert.doesNotMatch(enable, /disabled(?!=")|:disabled/);
|
||||||
assert.match(enable, /:aria-busy=/);
|
assert.match(enable, /:aria-busy=/);
|
||||||
assert.doesNotMatch(clear, /_clearSelectedLayersBtn\.disabled\s*=\s*true/);
|
|
||||||
assert.match(clear, /this\.setClearBusy\(true\)/);
|
assert.match(clear, /this\.setClearBusy\(true\)/);
|
||||||
const control = readFileSync(new URL('./ui/clearLayersControl.js', import.meta.url), 'utf8');
|
// The globe actions view marks the clear button busy, never disabled.
|
||||||
assert.match(control, /button\.setAttribute\('aria-busy', String\(busy\)\)/);
|
const globeActions = readFileSync(new URL('./ui/views/panels/GlobeActions.vue', import.meta.url), 'utf8');
|
||||||
assert.doesNotMatch(control, /button\.disabled\s*=/);
|
const clearButton = globeActions.match(/<button id="clear-selected-layers"[^>]*>/)?.[0];
|
||||||
|
assert.ok(clearButton, 'Clear selected layers button is missing');
|
||||||
|
assert.doesNotMatch(clearButton, /disabled(?!=")|:disabled=/);
|
||||||
|
assert.match(clearButton, /:aria-busy=/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Context header holds only its title and collapse control', () => {
|
test('Context header holds only its title and collapse control', () => {
|
||||||
|
|||||||
@@ -788,7 +788,8 @@ test('visual input listeners are revoked before asynchronous UI teardown', () =>
|
|||||||
assert.match(synchronous, /this\._displayBindings\.destroy\(\)/);
|
assert.match(synchronous, /this\._displayBindings\.destroy\(\)/);
|
||||||
assert.match(synchronous, /this\._visualSettings\.stop\(\)/);
|
assert.match(synchronous, /this\._visualSettings\.stop\(\)/);
|
||||||
assert.match(synchronous, /this\._mapSourceControls\?\.destroy\(\)/);
|
assert.match(synchronous, /this\._mapSourceControls\?\.destroy\(\)/);
|
||||||
assert.match(synchronous, /this\._clearLayersControl\?\.destroy\(\)/);
|
assert.match(synchronous, /globeActions\.detach\(this\._clearLayersToken\)/);
|
||||||
|
assert.match(synchronous, /globeActions\.detach\(this\._shareToken\)/);
|
||||||
assert.match(synchronous, /this\._locationNavigation\.destroy\(\)/);
|
assert.match(synchronous, /this\._locationNavigation\.destroy\(\)/);
|
||||||
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationControls\?\.destroy\(\)/);
|
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationControls\?\.destroy\(\)/);
|
||||||
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationLookup\?\.destroy\(\)/);
|
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationLookup\?\.destroy\(\)/);
|
||||||
|
|||||||
+29
-12
@@ -20,7 +20,6 @@ import { StreetLevelControls } from './streetLevelControls.js';
|
|||||||
import { RadioControls } from './radio.js';
|
import { RadioControls } from './radio.js';
|
||||||
import { LocalSdrControls } from './localSdrControls.js';
|
import { LocalSdrControls } from './localSdrControls.js';
|
||||||
import { LocationNavigation } from './locationNavigation.js';
|
import { LocationNavigation } from './locationNavigation.js';
|
||||||
import { bindClearLayersControl } from './layers.js';
|
|
||||||
import { bindCameraOrientationControls } from './cameraOrientationControls.js';
|
import { bindCameraOrientationControls } from './cameraOrientationControls.js';
|
||||||
import { createMapSourceControls } from './mapSource.js';
|
import { createMapSourceControls } from './mapSource.js';
|
||||||
import { STYLES } from './effects.js';
|
import { STYLES } from './effects.js';
|
||||||
@@ -63,6 +62,7 @@ import { useDisplayStore } from './stores/display.js';
|
|||||||
import { useLocaleStore } from './stores/locale.js';
|
import { useLocaleStore } from './stores/locale.js';
|
||||||
import { useLocationStore } from './stores/location.js';
|
import { useLocationStore } from './stores/location.js';
|
||||||
import { useCockpitStore } from './stores/cockpit.js';
|
import { useCockpitStore } from './stores/cockpit.js';
|
||||||
|
import { useGlobeActionsStore } from './stores/globeActions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central UI orchestrator for the God's Eye View application.
|
* Central UI orchestrator for the God's Eye View application.
|
||||||
@@ -1009,7 +1009,8 @@ export class StyleManager extends ShellFacade {
|
|||||||
if (!this._disposed) this._showToast(message);
|
if (!this._disposed) this._showToast(message);
|
||||||
},
|
},
|
||||||
setClearBusy: (busy) => {
|
setClearBusy: (busy) => {
|
||||||
if (!this._disposed) this._clearLayersControl?.setBusy(busy);
|
if (!this._disposed)
|
||||||
|
useGlobeActionsStore(applicationPinia()).publishClearBusy(busy);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1619,14 +1620,19 @@ export class StyleManager extends ShellFacade {
|
|||||||
return this._releaseFollowCamera({ preserveVesselSelection: false });
|
return this._releaseFollowCamera({ preserveVesselSelection: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wire the top-center action that clears only manager-owned data layers. */
|
/**
|
||||||
|
* Answer the top-center action that clears only manager-owned data layers;
|
||||||
|
* the caller owns its transaction, and the button stays focusable while it
|
||||||
|
* runs.
|
||||||
|
*/
|
||||||
_initClearSelectedLayersButton() {
|
_initClearSelectedLayersButton() {
|
||||||
if (!this._clearSelectedLayersBtn) return;
|
const store = useGlobeActionsStore(applicationPinia());
|
||||||
this._clearLayersControl?.destroy();
|
if (this._clearLayersToken) store.detach(this._clearLayersToken);
|
||||||
this._clearLayersControl = bindClearLayersControl(
|
this._clearLayersToken = store.connect({
|
||||||
this._clearSelectedLayersBtn,
|
clear: () => {
|
||||||
() => this.clearSelectedLayers(),
|
if (!this._disposed) void this.clearSelectedLayers();
|
||||||
);
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wire Google Maps-style tilt and north-up camera actions. */
|
/** Wire Google Maps-style tilt and north-up camera actions. */
|
||||||
@@ -1654,7 +1660,14 @@ export class StyleManager extends ShellFacade {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_initShareButton() {
|
_initShareButton() {
|
||||||
this._lifetime.listen(this._shareBtn, 'click', async () => {
|
const store = useGlobeActionsStore(applicationPinia());
|
||||||
|
if (this._shareToken) store.detach(this._shareToken);
|
||||||
|
this._shareToken = store.connect({ share: () => this._share() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Share or copy the current link, then say how it went. */
|
||||||
|
async _share() {
|
||||||
|
if (this._disposed) return;
|
||||||
const share = this.services.shareLink;
|
const share = this.services.shareLink;
|
||||||
let result;
|
let result;
|
||||||
if (typeof share === 'function') {
|
if (typeof share === 'function') {
|
||||||
@@ -1676,7 +1689,6 @@ export class StyleManager extends ShellFacade {
|
|||||||
? t('Link copied!')
|
? t('Link copied!')
|
||||||
: t('Copy failed'),
|
: t('Copy failed'),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HUD Toggle ───────────────────────────────
|
// ── HUD Toggle ───────────────────────────────
|
||||||
@@ -1778,7 +1790,12 @@ export class StyleManager extends ShellFacade {
|
|||||||
useDisplayStore(applicationPinia()).connectMapSource(null);
|
useDisplayStore(applicationPinia()).connectMapSource(null);
|
||||||
useDisplayStore(applicationPinia()).publishMapSource(null);
|
useDisplayStore(applicationPinia()).publishMapSource(null);
|
||||||
this._cameraOrientationControls?.destroy();
|
this._cameraOrientationControls?.destroy();
|
||||||
this._clearLayersControl?.destroy();
|
const globeActions = useGlobeActionsStore(applicationPinia());
|
||||||
|
if (this._clearLayersToken) globeActions.detach(this._clearLayersToken);
|
||||||
|
if (this._shareToken) globeActions.detach(this._shareToken);
|
||||||
|
this._clearLayersToken = this._shareToken = null;
|
||||||
|
// A clear this shell leaves running must not keep the next shell's bar busy.
|
||||||
|
globeActions.publishClearBusy(null);
|
||||||
this._cctvControls?.destroy();
|
this._cctvControls?.destroy();
|
||||||
this.services?.streetLevelLayer?.attachNavigation?.(null);
|
this.services?.streetLevelLayer?.attachNavigation?.(null);
|
||||||
this._streetLevelControls?.destroy();
|
this._streetLevelControls?.destroy();
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
|
|
||||||
/** Keep the clear action focusable while busy; the caller owns its transaction. */
|
/**
|
||||||
|
* Keep the clear action focusable while busy; the caller owns its
|
||||||
|
* transaction. The globe actions view renders the button from the globe
|
||||||
|
* actions store now; this stays exported from `./ui/layers` for hosts that
|
||||||
|
* imported it.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export function bindClearLayersControl(button, clear) {
|
export function bindClearLayersControl(button, clear) {
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
const click = () => {
|
const click = () => {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// src/ui/globeActions.test.mjs — the top-center globe actions call their
|
||||||
|
// owners through the globe actions store, and a clear under way keeps its
|
||||||
|
// button focusable and marks it busy.
|
||||||
|
import '../testSupport/registerDom.mjs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createApp, nextTick } from 'vue';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { useGlobeActionsStore } from './stores/globeActions.js';
|
||||||
|
import GlobeActions from './views/panels/GlobeActions.vue';
|
||||||
|
|
||||||
|
function mount(t) {
|
||||||
|
document.body.replaceChildren();
|
||||||
|
const pinia = createPinia();
|
||||||
|
const store = useGlobeActionsStore(pinia);
|
||||||
|
const calls = [];
|
||||||
|
store.connect({
|
||||||
|
clear: () => calls.push('clear'),
|
||||||
|
share: () => calls.push('share'),
|
||||||
|
});
|
||||||
|
const app = createApp(GlobeActions).use(pinia);
|
||||||
|
app.mount(document.body.appendChild(document.createElement('div')));
|
||||||
|
t.after(() => app.unmount());
|
||||||
|
return { calls, store };
|
||||||
|
}
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
test('the clear and share buttons call their owners', (t) => {
|
||||||
|
const { calls } = mount(t);
|
||||||
|
$('clear-selected-layers').click();
|
||||||
|
$('share-btn').click();
|
||||||
|
assert.deepEqual(calls, ['clear', 'share']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a clear under way marks the button busy and keeps it focusable', async (t) => {
|
||||||
|
const { store } = mount(t);
|
||||||
|
const clear = $('clear-selected-layers');
|
||||||
|
assert.equal(clear.getAttribute('aria-label'), 'Clear selected data layers');
|
||||||
|
assert.equal(clear.hasAttribute('aria-busy'), false);
|
||||||
|
store.publishClearBusy(true);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(clear.getAttribute('aria-busy'), 'true');
|
||||||
|
assert.equal(clear.getAttribute('aria-disabled'), 'true');
|
||||||
|
assert.equal(
|
||||||
|
clear.getAttribute('aria-label'),
|
||||||
|
'Clearing selected data layers',
|
||||||
|
);
|
||||||
|
assert.equal(clear.disabled, false);
|
||||||
|
store.publishClearBusy(false);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(clear.getAttribute('aria-busy'), 'false');
|
||||||
|
assert.equal(clear.getAttribute('aria-label'), 'Clear selected data layers');
|
||||||
|
// A shell disposed mid-clear hands the next one a bar that is not busy.
|
||||||
|
store.publishClearBusy(true);
|
||||||
|
store.publishClearBusy(null);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(clear.hasAttribute('aria-busy'), false);
|
||||||
|
assert.equal(clear.hasAttribute('aria-disabled'), false);
|
||||||
|
assert.equal(clear.getAttribute('aria-label'), 'Clear selected data layers');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a detached owner no longer answers', (t) => {
|
||||||
|
const { calls, store } = mount(t);
|
||||||
|
const token = store.connect({ share: () => calls.push('newer share') });
|
||||||
|
$('share-btn').click();
|
||||||
|
store.detach(token);
|
||||||
|
$('share-btn').click();
|
||||||
|
assert.deepEqual(calls, ['newer share', 'share']);
|
||||||
|
});
|
||||||
@@ -6,6 +6,8 @@ import { StyleManager } from './applicationShell.js';
|
|||||||
import { ShareLinkManager } from '../sharelink.js';
|
import { ShareLinkManager } from '../sharelink.js';
|
||||||
import { parseSharePath } from '../share/shareLinkPath.js';
|
import { parseSharePath } from '../share/shareLinkPath.js';
|
||||||
import { decodeShareView } from '../share/viewCodec.js';
|
import { decodeShareView } from '../share/viewCodec.js';
|
||||||
|
import { applicationPinia } from './vueRuntime.js';
|
||||||
|
import { useGlobeActionsStore } from './stores/globeActions.js';
|
||||||
|
|
||||||
/** The decoded view of a `/share/<code>` link. */
|
/** The decoded view of a `/share/<code>` link. */
|
||||||
const sharedState = (href) =>
|
const sharedState = (href) =>
|
||||||
@@ -36,20 +38,19 @@ function installClipboard(writeText) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wire the real share button handler onto a minimal shell; return a click. */
|
/** The real share action on a minimal shell; return a click. */
|
||||||
function shell({ manager, services = {} }) {
|
function shell({ manager, services = {} }) {
|
||||||
const toasts = [];
|
const toasts = [];
|
||||||
let click;
|
|
||||||
const owner = {
|
const owner = {
|
||||||
services,
|
services,
|
||||||
shareLinkManager: manager,
|
shareLinkManager: manager,
|
||||||
_shareBtn: {},
|
|
||||||
_disposed: false,
|
_disposed: false,
|
||||||
_lifetime: { listen: (_el, _type, handler) => (click = handler) },
|
|
||||||
_showToast: (message) => toasts.push(message),
|
_showToast: (message) => toasts.push(message),
|
||||||
};
|
};
|
||||||
StyleManager.prototype._initShareButton.call(owner);
|
return {
|
||||||
return { toasts, click: () => click() };
|
toasts,
|
||||||
|
click: () => StyleManager.prototype._share.call(owner),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('buildLink is a /share link stamped with its creation time, without ?lang', () => {
|
test('buildLink is a /share link stamped with its creation time, without ?lang', () => {
|
||||||
@@ -158,3 +159,23 @@ test('a host hook that answers without a promise, or throws, still gets a toast'
|
|||||||
assert.deepEqual(toasts, [toast]);
|
assert.deepEqual(toasts, [toast]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the shell answers the globe bar’s share and clear until it is disposed', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const owner = {
|
||||||
|
_disposed: false,
|
||||||
|
_share: () => calls.push('share'),
|
||||||
|
clearSelectedLayers: async () => calls.push('clear'),
|
||||||
|
};
|
||||||
|
StyleManager.prototype._initShareButton.call(owner);
|
||||||
|
StyleManager.prototype._initClearSelectedLayersButton.call(owner);
|
||||||
|
const store = useGlobeActionsStore(applicationPinia());
|
||||||
|
store.act('share');
|
||||||
|
store.act('clear');
|
||||||
|
assert.deepEqual(calls, ['share', 'clear']);
|
||||||
|
store.detach(owner._shareToken);
|
||||||
|
store.detach(owner._clearLayersToken);
|
||||||
|
store.act('share');
|
||||||
|
store.act('clear');
|
||||||
|
assert.deepEqual(calls, ['share', 'clear']);
|
||||||
|
});
|
||||||
@@ -63,10 +63,8 @@ export function readShellElements(document = globalThis.document) {
|
|||||||
_cctvSourceBadge: document.getElementById('cctv-source-badge'),
|
_cctvSourceBadge: document.getElementById('cctv-source-badge'),
|
||||||
_cctvMeta: document.getElementById('cctv-meta'),
|
_cctvMeta: document.getElementById('cctv-meta'),
|
||||||
_cctvSummary: document.getElementById('cctv-summary'),
|
_cctvSummary: document.getElementById('cctv-summary'),
|
||||||
_shareBtn: document.getElementById('share-btn'),
|
|
||||||
_tiltMapBtn: document.getElementById('tilt-map-view'),
|
_tiltMapBtn: document.getElementById('tilt-map-view'),
|
||||||
_northUpBtn: document.getElementById('north-up-view'),
|
_northUpBtn: document.getElementById('north-up-view'),
|
||||||
_clearSelectedLayersBtn: document.getElementById('clear-selected-layers'),
|
|
||||||
_resetGlobeBtn: document.getElementById('reset-globe-view'),
|
_resetGlobeBtn: document.getElementById('reset-globe-view'),
|
||||||
_cockpitResetGlobeBtn: document.getElementById('cockpit-reset-globe'),
|
_cockpitResetGlobeBtn: document.getElementById('cockpit-reset-globe'),
|
||||||
_locationSearch: document.getElementById('location-search'),
|
_locationSearch: document.getElementById('location-search'),
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { createOwners } from './owners.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top-center globe actions as their view renders them. The shell
|
||||||
|
* publishes whether a clear of the selected data layers is under way, and
|
||||||
|
* answers the clear and share buttons through act().
|
||||||
|
*/
|
||||||
|
export const useGlobeActionsStore = defineStore('gev-globe-actions', () => {
|
||||||
|
/** Whether a clear is under way; null until the shell first says. */
|
||||||
|
const clearBusy = ref(null);
|
||||||
|
|
||||||
|
/** @param {boolean | null} busy null when no shell answers the clear. */
|
||||||
|
function publishClearBusy(busy) {
|
||||||
|
clearBusy.value = busy == null ? null : Boolean(busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connect, detach, act } = createOwners();
|
||||||
|
|
||||||
|
return { clearBusy, publishClearBusy, connect, detach, act };
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@ import CelestialRing from './panels/CelestialRing.vue';
|
|||||||
import ScreenAnnotations from './panels/ScreenAnnotations.vue';
|
import ScreenAnnotations from './panels/ScreenAnnotations.vue';
|
||||||
import AwarenessDirection from './panels/AwarenessDirection.vue';
|
import AwarenessDirection from './panels/AwarenessDirection.vue';
|
||||||
import UiButton from '../kit/UiButton.vue';
|
import UiButton from '../kit/UiButton.vue';
|
||||||
|
import GlobeActions from './panels/GlobeActions.vue';
|
||||||
|
|
||||||
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
|
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
|
||||||
const { style } = storeToRefs(useDisplayStore());
|
const { style } = storeToRefs(useDisplayStore());
|
||||||
@@ -79,24 +80,7 @@ const vToastText = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top-center camera and share actions -->
|
<!-- Top-center camera and share actions -->
|
||||||
<nav id="top-center-actions" aria-label="Globe actions" data-i18n-aria-label>
|
<GlobeActions />
|
||||||
<button id="clear-selected-layers" type="button" aria-label="Clear selected data layers" title="Turn off all selected data layers" data-i18n-aria-label data-i18n-title>
|
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">layers_clear</span>
|
|
||||||
</button>
|
|
||||||
<button id="share-btn" type="button" aria-label="Copy share link" title="Copy share link" data-i18n-aria-label data-i18n-title>
|
|
||||||
<span class="share-icon" aria-hidden="true">🔗</span>
|
|
||||||
</button>
|
|
||||||
<button id="tilt-map-view" type="button" aria-label="Tilt map to oblique view" aria-pressed="false" title="Toggle straight-down and tilted map views" data-i18n-aria-label data-i18n-title>
|
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span>
|
|
||||||
</button>
|
|
||||||
<button id="north-up-view" type="button" aria-label="Reset map to north up" title="Reset map bearing to north" data-i18n-aria-label data-i18n-title>
|
|
||||||
<span class="camera-compass-n" aria-hidden="true">N</span>
|
|
||||||
<span class="camera-compass-needle material-symbols-outlined" aria-hidden="true">navigation</span>
|
|
||||||
</button>
|
|
||||||
<button id="reset-globe-view" type="button" aria-label="Reset to full globe view" title="Reset camera and return to full globe view" data-i18n-aria-label data-i18n-title>
|
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
<div id="global-loading-status" role="status" aria-live="polite" aria-atomic="true" :hidden="loading ? loading.hidden : true" :data-state="loading?.state ?? undefined">
|
<div id="global-loading-status" role="status" aria-live="polite" aria-atomic="true" :hidden="loading ? loading.hidden : true" :data-state="loading?.state ?? undefined">
|
||||||
<SplitFlapText id="global-loading-label" :text="loading ? loading.label : t('LOADING LIVE DATA')" :stopped="!!loading?.stopped" />
|
<SplitFlapText id="global-loading-label" :text="loading ? loading.label : t('LOADING LIVE DATA')" :stopped="!!loading?.stopped" />
|
||||||
<span id="global-loading-detail">{{ loading ? loading.detail : '' }}</span>
|
<span id="global-loading-detail">{{ loading ? loading.detail : '' }}</span>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { t } from '../../../i18n/index.js';
|
||||||
|
import { useGlobeActionsStore } from '../../stores/globeActions.js';
|
||||||
|
|
||||||
|
// The top-center camera and share actions. The shell answers the clear and
|
||||||
|
// share buttons through the store and publishes a clear under way, which
|
||||||
|
// keeps the button focusable and marks it busy.
|
||||||
|
const globe = useGlobeActionsStore();
|
||||||
|
const { clearBusy } = storeToRefs(globe);
|
||||||
|
const { act } = globe;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav id="top-center-actions" aria-label="Globe actions" data-i18n-aria-label>
|
||||||
|
<button id="clear-selected-layers" type="button" :aria-label="clearBusy ? t('Clearing selected data layers') : t('Clear selected data layers')" :aria-disabled="clearBusy == null ? undefined : String(clearBusy)" :aria-busy="clearBusy == null ? undefined : String(clearBusy)" title="Turn off all selected data layers" data-i18n-title @click="act('clear')">
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true">layers_clear</span>
|
||||||
|
</button>
|
||||||
|
<button id="share-btn" type="button" aria-label="Copy share link" title="Copy share link" data-i18n-aria-label data-i18n-title @click="act('share')">
|
||||||
|
<span class="share-icon" aria-hidden="true">🔗</span>
|
||||||
|
</button>
|
||||||
|
<button id="tilt-map-view" type="button" aria-label="Tilt map to oblique view" aria-pressed="false" title="Toggle straight-down and tilted map views" data-i18n-aria-label data-i18n-title>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span>
|
||||||
|
</button>
|
||||||
|
<button id="north-up-view" type="button" aria-label="Reset map to north up" title="Reset map bearing to north" data-i18n-aria-label data-i18n-title>
|
||||||
|
<span class="camera-compass-n" aria-hidden="true">N</span>
|
||||||
|
<span class="camera-compass-needle material-symbols-outlined" aria-hidden="true">navigation</span>
|
||||||
|
</button>
|
||||||
|
<button id="reset-globe-view" type="button" aria-label="Reset to full globe view" title="Reset camera and return to full globe view" data-i18n-aria-label data-i18n-title>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
Reference in new issue
Block a user