@jupytergis/base 0.13.1 → 0.13.2
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/lib/commands/BaseCommandIDs.d.ts +2 -4
- package/lib/commands/BaseCommandIDs.js +2 -4
- package/lib/commands/index.js +64 -55
- package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +1 -1
- package/lib/keybindings.json +4 -14
- package/lib/mainview/mainView.d.ts +2 -0
- package/lib/mainview/mainView.js +57 -53
- package/lib/panelview/components/layers.js +6 -1
- package/lib/panelview/leftpanel.js +20 -18
- package/lib/panelview/rightpanel.js +21 -19
- package/lib/panelview/story-maps/StoryViewerPanel.d.ts +9 -1
- package/lib/panelview/story-maps/StoryViewerPanel.js +54 -5
- package/package.json +2 -2
- package/style/shared/tabs.css +6 -0
|
@@ -16,10 +16,8 @@ export declare const newImageEntry = "jupytergis:newImageEntry";
|
|
|
16
16
|
export declare const newVideoEntry = "jupytergis:newVideoEntry";
|
|
17
17
|
export declare const newGeoTiffEntry = "jupytergis:newGeoTiffEntry";
|
|
18
18
|
export declare const newGeoParquetEntry = "jupytergis:newGeoParquetEntry";
|
|
19
|
-
export declare const
|
|
20
|
-
export declare const
|
|
21
|
-
export declare const renameGroup = "jupytergis:renameGroup";
|
|
22
|
-
export declare const removeGroup = "jupytergis:removeGroup";
|
|
19
|
+
export declare const renameSelected = "jupytergis:renameSelected";
|
|
20
|
+
export declare const removeSelected = "jupytergis:removeSelected";
|
|
23
21
|
export declare const moveLayersToGroup = "jupytergis:moveLayersToGroup";
|
|
24
22
|
export declare const moveLayerToNewGroup = "jupytergis:moveLayerToNewGroup";
|
|
25
23
|
export declare const renameSource = "jupytergis:renameSource";
|
|
@@ -25,10 +25,8 @@ export const newVideoEntry = 'jupytergis:newVideoEntry';
|
|
|
25
25
|
export const newGeoTiffEntry = 'jupytergis:newGeoTiffEntry';
|
|
26
26
|
export const newGeoParquetEntry = 'jupytergis:newGeoParquetEntry';
|
|
27
27
|
// Layer and group actions
|
|
28
|
-
export const
|
|
29
|
-
export const
|
|
30
|
-
export const renameGroup = 'jupytergis:renameGroup';
|
|
31
|
-
export const removeGroup = 'jupytergis:removeGroup';
|
|
28
|
+
export const renameSelected = 'jupytergis:renameSelected';
|
|
29
|
+
export const removeSelected = 'jupytergis:removeSelected';
|
|
32
30
|
export const moveLayersToGroup = 'jupytergis:moveLayersToGroup';
|
|
33
31
|
export const moveLayerToNewGroup = 'jupytergis:moveLayerToNewGroup';
|
|
34
32
|
// Source actions
|
package/lib/commands/index.js
CHANGED
|
@@ -361,41 +361,40 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
361
361
|
/**
|
|
362
362
|
* LAYERS and LAYER GROUP actions.
|
|
363
363
|
*/
|
|
364
|
-
commands.addCommand(CommandIDs.
|
|
365
|
-
label: trans.__('Rename
|
|
366
|
-
|
|
367
|
-
var _a;
|
|
364
|
+
commands.addCommand(CommandIDs.renameSelected, {
|
|
365
|
+
label: trans.__('Rename'),
|
|
366
|
+
isEnabled: () => {
|
|
367
|
+
var _a, _b, _c;
|
|
368
368
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
369
|
-
|
|
369
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
370
|
+
return !!selected && Object.keys(selected).length === 1;
|
|
370
371
|
},
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
label: trans.__('Remove Layer'),
|
|
374
|
-
execute: () => {
|
|
375
|
-
var _a;
|
|
372
|
+
execute: async () => {
|
|
373
|
+
var _a, _b, _c;
|
|
376
374
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
376
|
+
if (!model || !selected) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await Private.renameSelectedItem(model);
|
|
381
380
|
},
|
|
382
381
|
});
|
|
383
|
-
commands.addCommand(CommandIDs.
|
|
384
|
-
label: trans.__('
|
|
385
|
-
|
|
386
|
-
var _a;
|
|
382
|
+
commands.addCommand(CommandIDs.removeSelected, {
|
|
383
|
+
label: trans.__('Remove'),
|
|
384
|
+
isEnabled: () => {
|
|
385
|
+
var _a, _b, _c;
|
|
387
386
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
388
|
-
|
|
387
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
388
|
+
return !!selected && Object.keys(selected).length > 0;
|
|
389
389
|
},
|
|
390
|
-
});
|
|
391
|
-
commands.addCommand(CommandIDs.removeGroup, {
|
|
392
|
-
label: trans.__('Remove Group'),
|
|
393
390
|
execute: async () => {
|
|
394
|
-
var _a;
|
|
391
|
+
var _a, _b, _c;
|
|
395
392
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
393
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
394
|
+
if (!model || !selected) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
await Private.removeSelectedItems(model);
|
|
399
398
|
},
|
|
400
399
|
});
|
|
401
400
|
commands.addCommand(CommandIDs.moveLayersToGroup, {
|
|
@@ -473,7 +472,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
473
472
|
execute: async () => {
|
|
474
473
|
var _a;
|
|
475
474
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
476
|
-
await Private.renameSelectedItem(model
|
|
475
|
+
await Private.renameSelectedItem(model);
|
|
477
476
|
},
|
|
478
477
|
});
|
|
479
478
|
commands.addCommand(CommandIDs.removeSource, {
|
|
@@ -481,15 +480,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
481
480
|
execute: () => {
|
|
482
481
|
var _a;
|
|
483
482
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
484
|
-
Private.
|
|
485
|
-
var _a;
|
|
486
|
-
if (!((_a = model === null || model === void 0 ? void 0 : model.getLayersBySource(selection).length) !== null && _a !== void 0 ? _a : true)) {
|
|
487
|
-
model === null || model === void 0 ? void 0 : model.sharedModel.removeSource(selection);
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
showErrorMessage('Remove source error', 'The source is used by a layer.');
|
|
491
|
-
}
|
|
492
|
-
});
|
|
483
|
+
Private.removeSelectedSources(model);
|
|
493
484
|
},
|
|
494
485
|
});
|
|
495
486
|
// Console commands
|
|
@@ -934,42 +925,60 @@ var Private;
|
|
|
934
925
|
};
|
|
935
926
|
}
|
|
936
927
|
Private.createEntry = createEntry;
|
|
937
|
-
function removeSelectedItems(model
|
|
928
|
+
function removeSelectedItems(model) {
|
|
938
929
|
var _a, _b;
|
|
939
930
|
const selected = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
940
|
-
if (!selected) {
|
|
931
|
+
if (!selected || !model) {
|
|
941
932
|
console.error('Failed to remove selected item -- nothing selected');
|
|
942
933
|
return;
|
|
943
934
|
}
|
|
944
|
-
for (const
|
|
945
|
-
|
|
946
|
-
|
|
935
|
+
for (const id of Object.keys(selected)) {
|
|
936
|
+
const item = selected[id];
|
|
937
|
+
switch (item.type) {
|
|
938
|
+
case 'layer':
|
|
939
|
+
model.removeLayer(id);
|
|
940
|
+
break;
|
|
941
|
+
case 'group':
|
|
942
|
+
model.removeLayerGroup(id);
|
|
943
|
+
break;
|
|
947
944
|
}
|
|
948
945
|
}
|
|
949
946
|
}
|
|
950
947
|
Private.removeSelectedItems = removeSelectedItems;
|
|
951
|
-
async function renameSelectedItem(model
|
|
952
|
-
var _a;
|
|
953
|
-
const selectedItems = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected.value;
|
|
948
|
+
async function renameSelectedItem(model) {
|
|
949
|
+
var _a, _b;
|
|
950
|
+
const selectedItems = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
954
951
|
if (!selectedItems || !model) {
|
|
955
|
-
console.error(
|
|
952
|
+
console.error('No item selected');
|
|
956
953
|
return;
|
|
957
954
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
if (selectedItems[id].type === itemType) {
|
|
962
|
-
itemId = id;
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
955
|
+
const ids = Object.keys(selectedItems);
|
|
956
|
+
if (ids.length === 0) {
|
|
957
|
+
return;
|
|
965
958
|
}
|
|
966
|
-
|
|
959
|
+
const itemId = ids[0];
|
|
960
|
+
const item = selectedItems[itemId];
|
|
961
|
+
if (!item.type) {
|
|
967
962
|
return;
|
|
968
963
|
}
|
|
969
|
-
|
|
970
|
-
model.setEditingItem(itemType, itemId);
|
|
964
|
+
model.setEditingItem(item.type, itemId);
|
|
971
965
|
}
|
|
972
966
|
Private.renameSelectedItem = renameSelectedItem;
|
|
967
|
+
function removeSelectedSources(model) {
|
|
968
|
+
var _a, _b;
|
|
969
|
+
const selected = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
970
|
+
if (!selected || !model) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
for (const id of Object.keys(selected)) {
|
|
974
|
+
if (model.getLayersBySource(id).length > 0) {
|
|
975
|
+
showErrorMessage('Remove source error', 'The source is used by a layer.');
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
model.sharedModel.removeSource(id);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
Private.removeSelectedSources = removeSelectedSources;
|
|
973
982
|
function executeConsole(tracker) {
|
|
974
983
|
const current = tracker.currentWidget;
|
|
975
984
|
if (!current || !(current instanceof JupyterGISDocumentWidget)) {
|
package/lib/keybindings.json
CHANGED
|
@@ -30,24 +30,14 @@
|
|
|
30
30
|
"selector": ".data-jgis-keybinding .jp-gis-source"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
"command": "jupytergis:
|
|
33
|
+
"command": "jupytergis:removeSelected",
|
|
34
34
|
"keys": ["Delete"],
|
|
35
|
-
"selector": ".data-jgis-keybinding
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"command": "jupytergis:renameLayer",
|
|
39
|
-
"keys": ["F2"],
|
|
40
|
-
"selector": ".jp-gis-layerItem"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"command": "jupytergis:removeGroup",
|
|
44
|
-
"keys": ["Delete"],
|
|
45
|
-
"selector": ".data-jgis-keybinding .jp-gis-layerGroupHeader"
|
|
35
|
+
"selector": ".data-jgis-keybinding"
|
|
46
36
|
},
|
|
47
37
|
{
|
|
48
|
-
"command": "jupytergis:
|
|
38
|
+
"command": "jupytergis:renameSelected",
|
|
49
39
|
"keys": ["F2"],
|
|
50
|
-
"selector": ".
|
|
40
|
+
"selector": ".data-jgis-keybinding"
|
|
51
41
|
},
|
|
52
42
|
{
|
|
53
43
|
"command": "jupytergis:executeConsole",
|
|
@@ -225,6 +225,8 @@ export declare class MainView extends React.Component<IProps, IStates> {
|
|
|
225
225
|
private _featurePropertyCache;
|
|
226
226
|
private _isSpectaPresentationInitialized;
|
|
227
227
|
private _storyScrollHandler;
|
|
228
|
+
private _clearStoryScrollGuard;
|
|
229
|
+
private _pendingStoryScrollRafId;
|
|
228
230
|
}
|
|
229
231
|
/** Thin wrapper that injects isMobile from useMediaQuery so MainView can use it in JSX. */
|
|
230
232
|
declare function MainViewWithMediaQuery(props: IProps): React.JSX.Element;
|
package/lib/mainview/mainView.js
CHANGED
|
@@ -447,72 +447,75 @@ export class MainView extends React.Component {
|
|
|
447
447
|
});
|
|
448
448
|
};
|
|
449
449
|
this._setupStoryScrollListener = () => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const handleScroll = (e) => {
|
|
450
|
+
var _a, _b;
|
|
451
|
+
// Guard: block wheel-driven segment change until transition has ended
|
|
452
|
+
let segmentChangeInProgress = false;
|
|
453
|
+
const clearGuard = () => {
|
|
454
|
+
segmentChangeInProgress = false;
|
|
455
|
+
};
|
|
456
|
+
this._clearStoryScrollGuard = clearGuard;
|
|
457
|
+
let accumulatedDeltaY = 0;
|
|
458
|
+
let scrollContainer = (_b = (_a = this.storyViewerPanelRef.current) === null || _a === void 0 ? void 0 : _a.getScrollContainer()) !== null && _b !== void 0 ? _b : null;
|
|
459
|
+
const processStoryScrollFrame = () => {
|
|
460
|
+
this._pendingStoryScrollRafId = null;
|
|
462
461
|
const currentPanelHandle = this.storyViewerPanelRef.current;
|
|
463
|
-
if (!currentPanelHandle || !
|
|
462
|
+
if (!currentPanelHandle || !scrollContainer) {
|
|
463
|
+
accumulatedDeltaY = 0;
|
|
464
464
|
return;
|
|
465
465
|
}
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
466
|
+
const deltaY = accumulatedDeltaY;
|
|
467
|
+
accumulatedDeltaY = 0;
|
|
468
|
+
const isScrollingUp = deltaY < 0;
|
|
469
|
+
const isScrollingDown = deltaY > 0;
|
|
470
|
+
const isAtTop = currentPanelHandle.getAtTop();
|
|
471
|
+
const isAtBottom = currentPanelHandle.getAtBottom();
|
|
472
|
+
const hasOverflow = !(isAtTop && isAtBottom);
|
|
473
|
+
const canGoInDirection = (isScrollingDown && currentPanelHandle.hasNext) ||
|
|
474
|
+
(isScrollingUp && currentPanelHandle.hasPrev);
|
|
475
|
+
const atEdge = (isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop);
|
|
476
|
+
const wantSegmentChange = canGoInDirection && (!hasOverflow || atEdge);
|
|
477
|
+
if (wantSegmentChange) {
|
|
478
|
+
if (segmentChangeInProgress) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
segmentChangeInProgress = true;
|
|
482
|
+
isScrollingDown
|
|
483
|
+
? currentPanelHandle.handleNext()
|
|
484
|
+
: currentPanelHandle.handlePrev();
|
|
474
485
|
return;
|
|
475
486
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
487
|
+
scrollContainer.scrollBy({ top: deltaY });
|
|
488
|
+
};
|
|
489
|
+
const handleScroll = (event) => {
|
|
490
|
+
var _a, _b;
|
|
491
|
+
event.preventDefault();
|
|
492
|
+
if (!scrollContainer || !document.contains(scrollContainer)) {
|
|
493
|
+
scrollContainer =
|
|
494
|
+
(_b = (_a = this.storyViewerPanelRef.current) === null || _a === void 0 ? void 0 : _a.getScrollContainer()) !== null && _b !== void 0 ? _b : null;
|
|
482
495
|
}
|
|
483
|
-
|
|
484
|
-
const scrollTop = storyViewerPanel.scrollTop;
|
|
485
|
-
const scrollHeight = storyViewerPanel.scrollHeight;
|
|
486
|
-
const clientHeight = storyViewerPanel.clientHeight;
|
|
487
|
-
const isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_EDGE_THRESHOLD;
|
|
488
|
-
const isAtTop = scrollTop <= SCROLL_EDGE_THRESHOLD;
|
|
489
|
-
const isScrollingDown = wheelEvent.deltaY > 0;
|
|
490
|
-
const isScrollingUp = wheelEvent.deltaY < 0;
|
|
491
|
-
// At edges: change segments
|
|
492
|
-
if ((isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop)) {
|
|
493
|
-
wheelEvent.preventDefault();
|
|
494
|
-
isScrollingDown ? throttledHandleNext() : throttledHandlePrev();
|
|
496
|
+
if (!scrollContainer) {
|
|
495
497
|
return;
|
|
496
498
|
}
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
499
|
+
// One physical scroll tick often fires ~4 wheel events (sometimes across
|
|
500
|
+
// frames on slow hardware). We accumulate deltaY and run flush once per
|
|
501
|
+
// frame via rAF—the frame boundary batches events without adding delay.
|
|
502
|
+
// So one scroll means one segment/scroll decision.
|
|
503
|
+
accumulatedDeltaY += event.deltaY;
|
|
504
|
+
if (this._pendingStoryScrollRafId === null) {
|
|
505
|
+
this._pendingStoryScrollRafId = requestAnimationFrame(processStoryScrollFrame);
|
|
500
506
|
}
|
|
501
|
-
// Scrolling outside the panel: forward scroll to panel (no throttling for smooth scrolling)
|
|
502
|
-
wheelEvent.preventDefault();
|
|
503
|
-
const newScrollTop = Math.max(0, Math.min(scrollHeight - clientHeight, scrollTop + wheelEvent.deltaY));
|
|
504
|
-
storyViewerPanel.scrollTop = newScrollTop;
|
|
505
507
|
};
|
|
506
508
|
this._storyScrollHandler = handleScroll;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
containerElement.addEventListener('wheel', handleScroll, {
|
|
511
|
-
passive: false,
|
|
512
|
-
});
|
|
509
|
+
const container = document.querySelector('.jGIS-Mainview-Container');
|
|
510
|
+
if (container) {
|
|
511
|
+
container.addEventListener('wheel', handleScroll, { passive: false });
|
|
513
512
|
}
|
|
514
513
|
};
|
|
515
514
|
this._cleanupStoryScrollListener = () => {
|
|
515
|
+
if (this._pendingStoryScrollRafId !== null) {
|
|
516
|
+
cancelAnimationFrame(this._pendingStoryScrollRafId);
|
|
517
|
+
this._pendingStoryScrollRafId = null;
|
|
518
|
+
}
|
|
516
519
|
if (this._storyScrollHandler) {
|
|
517
520
|
const containerElement = document.querySelector('.jGIS-Mainview-Container');
|
|
518
521
|
if (containerElement) {
|
|
@@ -596,6 +599,7 @@ export class MainView extends React.Component {
|
|
|
596
599
|
this._featurePropertyCache = new Map();
|
|
597
600
|
this._isSpectaPresentationInitialized = false;
|
|
598
601
|
this._storyScrollHandler = null;
|
|
602
|
+
this._pendingStoryScrollRafId = null;
|
|
599
603
|
this._state = props.state;
|
|
600
604
|
this._formSchemaRegistry = props.formSchemaRegistry;
|
|
601
605
|
this._annotationModel = props.annotationModel;
|
|
@@ -2094,7 +2098,7 @@ export class MainView extends React.Component {
|
|
|
2094
2098
|
this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state, settings: this.state.jgisSettings })),
|
|
2095
2099
|
this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, commands: this._mainViewModel.commands, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel, addLayer: this.addLayer.bind(this), removeLayer: this.removeLayer.bind(this), settings: this.state.jgisSettings })))) : this.props.isMobile ? (React.createElement(MobileSpectaPanel, { model: this._model })) : (React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
|
|
2096
2100
|
React.createElement("div", { ref: this.spectaContainerRef, className: "jgis-specta-story-panel-container" },
|
|
2097
|
-
React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod" }))))),
|
|
2101
|
+
React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod", onSegmentTransitionEnd: () => this._clearStoryScrollGuard() }))))),
|
|
2098
2102
|
React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
|
|
2099
2103
|
!this.state.isSpectaPresentation && (React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })))));
|
|
2100
2104
|
}
|
|
@@ -345,6 +345,11 @@ const LayerComponent = props => {
|
|
|
345
345
|
const moveToExtent = () => {
|
|
346
346
|
gisModel === null || gisModel === void 0 ? void 0 : gisModel.centerOnPosition(layerId);
|
|
347
347
|
};
|
|
348
|
+
const handleDoubleClick = (e) => {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
e.stopPropagation();
|
|
351
|
+
moveToExtent();
|
|
352
|
+
};
|
|
348
353
|
const getSlideNumber = () => {
|
|
349
354
|
if (!gisModel) {
|
|
350
355
|
return;
|
|
@@ -373,7 +378,7 @@ const LayerComponent = props => {
|
|
|
373
378
|
padding: '2px 4px',
|
|
374
379
|
fontSize: 'inherit',
|
|
375
380
|
fontFamily: 'inherit',
|
|
376
|
-
}, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2 }, name)),
|
|
381
|
+
}, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2, onDoubleClick: handleDoubleClick, title: "Double-click to zoom to layer" }, name)),
|
|
377
382
|
React.createElement(Button, { title: 'Move map to the extent of the layer', onClick: moveToExtent, minimal: true },
|
|
378
383
|
React.createElement(LabIcon.resolveReact, { icon: targetWithCenterIcon, className: LAYER_ICON_CLASS, tag: "span" }))),
|
|
379
384
|
expanded && gisModel && hasSupportedSymbology && (React.createElement("div", { style: { marginTop: 6, width: '100%' } },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import Draggable from 'react-draggable';
|
|
2
3
|
import { CommandIDs } from '../constants';
|
|
3
4
|
import { LayersBodyComponent } from './components/layers';
|
|
4
5
|
import FilterComponent from './filter-panel/Filter';
|
|
@@ -123,22 +124,23 @@ export const LeftPanel = (props) => {
|
|
|
123
124
|
props.settings.filtersDisabled &&
|
|
124
125
|
props.settings.storyMapsDisabled;
|
|
125
126
|
const leftPanelVisible = !props.settings.leftPanelDisabled && !allLeftTabsDisabled;
|
|
126
|
-
return (React.createElement(
|
|
127
|
-
React.createElement(
|
|
128
|
-
React.createElement(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
React.createElement(
|
|
138
|
-
|
|
139
|
-
React.createElement(
|
|
140
|
-
|
|
141
|
-
React.createElement(
|
|
142
|
-
|
|
143
|
-
React.createElement(
|
|
127
|
+
return (React.createElement(Draggable, { handle: ".jgis-tabs-list", cancel: ".jgis-tabs-trigger", bounds: ".jGIS-Mainview-Container" },
|
|
128
|
+
React.createElement("div", { className: "jgis-left-panel-container", style: { display: leftPanelVisible ? 'block' : 'none' } },
|
|
129
|
+
React.createElement(PanelTabs, { curTab: curTab, className: "jgis-panel-tabs" },
|
|
130
|
+
React.createElement(TabsList, null, tabInfo.map(tab => (React.createElement(TabsTrigger, { className: "jGIS-layer-browser-category", key: tab.name, value: tab.name, onClick: () => {
|
|
131
|
+
if (curTab !== tab.name) {
|
|
132
|
+
setCurTab(tab.name);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
setCurTab('');
|
|
136
|
+
}
|
|
137
|
+
} }, tab.title)))),
|
|
138
|
+
!props.settings.layersDisabled && (React.createElement(TabsContent, { value: "layers", className: "jgis-panel-tab-content jp-gis-layerPanel" },
|
|
139
|
+
React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: filteredLayerTree }))),
|
|
140
|
+
!props.settings.stacBrowserDisabled && (React.createElement(TabsContent, { value: "stac", className: "jgis-panel-tab-content jgis-panel-tab-content-stac-panel" },
|
|
141
|
+
React.createElement(StacPanel, { model: props.model }))),
|
|
142
|
+
!props.settings.filtersDisabled && (React.createElement(TabsContent, { value: "filters", className: "jgis-panel-tab-content" },
|
|
143
|
+
React.createElement(FilterComponent, { model: props.model }))),
|
|
144
|
+
!props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "segments", className: "jgis-panel-tab-content" },
|
|
145
|
+
React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: storySegmentLayerTree })))))));
|
|
144
146
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import Draggable from 'react-draggable';
|
|
2
3
|
import { AnnotationsPanel } from './annotationPanel';
|
|
3
4
|
import { IdentifyPanelComponent } from './identify-panel/IdentifyPanel';
|
|
4
5
|
import { ObjectPropertiesReact } from './objectproperties';
|
|
@@ -74,23 +75,24 @@ export const RightPanel = props => {
|
|
|
74
75
|
const toggleEditor = () => {
|
|
75
76
|
setEditorMode(!editorMode);
|
|
76
77
|
};
|
|
77
|
-
return (React.createElement(
|
|
78
|
-
React.createElement(
|
|
79
|
-
React.createElement(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
React.createElement(
|
|
89
|
-
|
|
90
|
-
!
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
React.createElement(
|
|
94
|
-
|
|
95
|
-
React.createElement(
|
|
78
|
+
return (React.createElement(Draggable, { handle: ".jgis-tabs-list", cancel: ".jgis-tabs-trigger", bounds: ".jGIS-Mainview-Container" },
|
|
79
|
+
React.createElement("div", { className: "jgis-right-panel-container", style: { display: rightPanelVisible ? 'block' : 'none' } },
|
|
80
|
+
React.createElement(PanelTabs, { className: "jgis-panel-tabs", curTab: curTab },
|
|
81
|
+
React.createElement(TabsList, null, tabInfo.map(tab => (React.createElement(TabsTrigger, { className: "jGIS-layer-browser-category", key: `${tab.name}-${tab.title}`, value: tab.name, onClick: () => {
|
|
82
|
+
if (curTab !== tab.name) {
|
|
83
|
+
setCurTab(tab.name);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setCurTab('');
|
|
87
|
+
}
|
|
88
|
+
} }, tab.title)))),
|
|
89
|
+
!props.settings.objectPropertiesDisabled && (React.createElement(TabsContent, { value: "objectProperties", className: "jgis-panel-tab-content" },
|
|
90
|
+
React.createElement(ObjectPropertiesReact, { setSelectedObject: setSelectedObjectProperties, selectedObject: selectedObjectProperties, formSchemaRegistry: props.formSchemaRegistry, model: props.model }))),
|
|
91
|
+
!props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "storyPanel", className: "jgis-panel-tab-content", style: { paddingTop: 0 } },
|
|
92
|
+
!storyMapPresentationMode && (React.createElement(PreviewModeSwitch, { checked: !editorMode, onCheckedChange: toggleEditor })),
|
|
93
|
+
showEditor ? (React.createElement(StoryEditorPanel, { model: props.model, commands: props.commands })) : (React.createElement(StoryViewerPanel, { model: props.model, isSpecta: false, addLayer: props.addLayer, removeLayer: props.removeLayer })))),
|
|
94
|
+
!props.settings.annotationsDisabled && (React.createElement(TabsContent, { value: "annotations", className: "jgis-panel-tab-content" },
|
|
95
|
+
React.createElement(AnnotationsPanel, { annotationModel: props.annotationModel, jgisModel: props.model }))),
|
|
96
|
+
!props.settings.identifyDisabled && (React.createElement(TabsContent, { value: "identifyPanel", className: "jgis-panel-tab-content" },
|
|
97
|
+
React.createElement(IdentifyPanelComponent, { model: props.model })))))));
|
|
96
98
|
};
|
|
@@ -7,11 +7,19 @@ interface IStoryViewerPanelProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
|
|
9
9
|
removeLayer?: (id: string) => void;
|
|
10
|
+
/** Called when the segment transition animation has finished (e.g. for scroll-guard cleanup). */
|
|
11
|
+
onSegmentTransitionEnd?: () => void;
|
|
10
12
|
}
|
|
11
13
|
export interface IStoryViewerPanelHandle {
|
|
12
14
|
handlePrev: () => void;
|
|
13
15
|
handleNext: () => void;
|
|
14
|
-
|
|
16
|
+
spectaMode: boolean;
|
|
17
|
+
hasPrev: boolean;
|
|
18
|
+
hasNext: boolean;
|
|
19
|
+
getAtTop: () => boolean;
|
|
20
|
+
getAtBottom: () => boolean;
|
|
21
|
+
/** The scrollable panel DOM element (same instance for all segments). */
|
|
22
|
+
getScrollContainer: () => HTMLDivElement | null;
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* Where the story nav bar should be rendered in the viewer layout.
|
|
@@ -18,12 +18,17 @@ function getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile) {
|
|
|
18
18
|
}
|
|
19
19
|
return hasImage ? 'over-image' : 'below-title';
|
|
20
20
|
}
|
|
21
|
-
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer }, ref) => {
|
|
21
|
+
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer, onSegmentTransitionEnd, }, ref) => {
|
|
22
22
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
23
23
|
const [currentIndexDisplayed, setCurrentIndexDisplayed] = useState(() => model.getCurrentSegmentIndex());
|
|
24
24
|
const [storyData, setStoryData] = useState((_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null);
|
|
25
25
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
26
26
|
const panelRef = useRef(null);
|
|
27
|
+
const segmentContainerRef = useRef(null);
|
|
28
|
+
const topSentinelRef = useRef(null);
|
|
29
|
+
const bottomSentinelRef = useRef(null);
|
|
30
|
+
const atTopRef = useRef(false);
|
|
31
|
+
const atBottomRef = useRef(false);
|
|
27
32
|
const setIndex = useCallback((index) => {
|
|
28
33
|
model.setCurrentSegmentIndex(index);
|
|
29
34
|
setCurrentIndexDisplayed(index);
|
|
@@ -283,20 +288,63 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
283
288
|
hasPrev,
|
|
284
289
|
hasNext,
|
|
285
290
|
};
|
|
291
|
+
// IntersectionObserver for at-top/at-bottom (avoids layout reads in scroll path)
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const root = panelRef.current;
|
|
294
|
+
const topEl = topSentinelRef.current;
|
|
295
|
+
const bottomEl = bottomSentinelRef.current;
|
|
296
|
+
if (!root || !topEl || !bottomEl) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const observer = new IntersectionObserver((entries) => {
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.target === topEl) {
|
|
302
|
+
atTopRef.current = entry.isIntersecting;
|
|
303
|
+
}
|
|
304
|
+
else if (entry.target === bottomEl) {
|
|
305
|
+
atBottomRef.current = entry.isIntersecting;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, { root, threshold: 0, rootMargin: '0px' });
|
|
309
|
+
observer.observe(topEl);
|
|
310
|
+
observer.observe(bottomEl);
|
|
311
|
+
return () => observer.disconnect();
|
|
312
|
+
}, [currentIndexDisplayed]);
|
|
286
313
|
// Expose methods via ref for parent component to use
|
|
287
314
|
useImperativeHandle(ref, () => ({
|
|
288
315
|
handlePrev,
|
|
289
316
|
handleNext,
|
|
290
|
-
|
|
291
|
-
|
|
317
|
+
spectaMode: isSpecta,
|
|
318
|
+
hasPrev,
|
|
319
|
+
hasNext,
|
|
320
|
+
getAtTop: () => atTopRef.current,
|
|
321
|
+
getAtBottom: () => atBottomRef.current,
|
|
322
|
+
getScrollContainer: () => panelRef.current,
|
|
323
|
+
}), [handlePrev, handleNext, storyData, isSpecta, hasPrev, hasNext]);
|
|
292
324
|
const hasImage = !!(((_d = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _d === void 0 ? void 0 : _d.image) && imageLoaded);
|
|
293
325
|
const storyType = (_e = storyData.storyType) !== null && _e !== void 0 ? _e : 'guided';
|
|
294
326
|
const navPlacement = getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile);
|
|
295
327
|
const navSlot = navPlacement !== null ? (React.createElement(StoryNavBar, Object.assign({ placement: navPlacement }, storyNavBarProps))) : null;
|
|
296
328
|
// Get transition time from current segment, default to 0.3s
|
|
297
329
|
const transitionTime = (_g = (_f = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.transition) === null || _f === void 0 ? void 0 : _f.time) !== null && _g !== void 0 ? _g : 0.3;
|
|
330
|
+
// Notify parent when segment transition animation ends (e.g. for scroll-guard cleanup)
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const el = segmentContainerRef.current;
|
|
333
|
+
if (!el || !onSegmentTransitionEnd) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const handleAnimationEnd = (e) => {
|
|
337
|
+
if (e.animationName === 'fadeIn') {
|
|
338
|
+
el.removeEventListener('animationend', handleAnimationEnd);
|
|
339
|
+
onSegmentTransitionEnd();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
el.addEventListener('animationend', handleAnimationEnd);
|
|
343
|
+
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
|
344
|
+
}, [currentIndexDisplayed, onSegmentTransitionEnd]);
|
|
298
345
|
return (React.createElement("div", { ref: panelRef, className: cn('jgis-story-viewer-panel', className), id: "jgis-story-segment-panel" },
|
|
299
|
-
React.createElement("div", {
|
|
346
|
+
React.createElement("div", { ref: topSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "top", style: { height: 1, minHeight: 1, pointerEvents: 'none' } }),
|
|
347
|
+
React.createElement("div", { ref: segmentContainerRef, key: currentIndexDisplayed, className: "jgis-story-segment-container", style: {
|
|
300
348
|
animationDuration: `${transitionTime}s`,
|
|
301
349
|
} },
|
|
302
350
|
React.createElement("div", { id: "jgis-story-segment-header" },
|
|
@@ -307,7 +355,8 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
307
355
|
? navSlot
|
|
308
356
|
: null })),
|
|
309
357
|
React.createElement("div", { id: "jgis-story-segment-content" },
|
|
310
|
-
React.createElement(StoryContentSection, { markdown: (_o = (_m = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _m === void 0 ? void 0 : _m.markdown) !== null && _o !== void 0 ? _o : '' })))
|
|
358
|
+
React.createElement(StoryContentSection, { markdown: (_o = (_m = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _m === void 0 ? void 0 : _m.markdown) !== null && _o !== void 0 ? _o : '' }))),
|
|
359
|
+
React.createElement("div", { ref: bottomSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "bottom", style: { height: 1, minHeight: 1, pointerEvents: 'none' } })));
|
|
311
360
|
});
|
|
312
361
|
StoryViewerPanel.displayName = 'StoryViewerPanel';
|
|
313
362
|
export default StoryViewerPanel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupytergis/base",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "A JupyterLab extension for 3D modelling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@jupyter/collaboration": "^4",
|
|
45
45
|
"@jupyter/react-components": "^0.16.6",
|
|
46
46
|
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
|
|
47
|
-
"@jupytergis/schema": "^0.13.
|
|
47
|
+
"@jupytergis/schema": "^0.13.2",
|
|
48
48
|
"@jupyterlab/application": "^4.3.0",
|
|
49
49
|
"@jupyterlab/apputils": "^4.3.0",
|
|
50
50
|
"@jupyterlab/completer": "^4.3.0",
|
package/style/shared/tabs.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
align-items: center;
|
|
6
6
|
background-color: var(--jp-layout-color0);
|
|
7
7
|
}
|
|
8
|
+
|
|
8
9
|
.jgis-tabs-list {
|
|
9
10
|
display: inline-flex;
|
|
10
11
|
height: 2.5rem;
|
|
@@ -12,12 +13,17 @@
|
|
|
12
13
|
justify-content: space-evenly;
|
|
13
14
|
background-color: var(--jp-layout-color2);
|
|
14
15
|
color: var(--jp-ui-font-color0);
|
|
16
|
+
cursor: grab;
|
|
15
17
|
gap: 1rem;
|
|
16
18
|
width: 100%;
|
|
17
19
|
font-size: 9px;
|
|
18
20
|
overflow-x: scroll;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
.jgis-tabs-list:active {
|
|
24
|
+
cursor: grabbing;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
.jgis-tabs-trigger {
|
|
22
28
|
display: inline-flex;
|
|
23
29
|
align-items: center;
|