@finos/legend-application-marketplace
Version:
Legend Marketplace application core
266 lines (242 loc) • 9.56 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NAVIGATION_ZONE_SEPARATOR } from '@finos/legend-application';
import { action, computed, makeObservable, observable } from 'mobx';
import { type DataProductViewerState } from './DataProductViewerState.js';
import { at, guaranteeNonNullable, isNonNullable } from '@finos/legend-shared';
import {
DATA_PRODUCT_VIEWER_ACTIVITY_MODE,
extractActivityFromAnchor,
generateAnchorForActivity,
} from './DataProductViewerNavigation.js';
export const DATA_PRODUCT_WIKI_PAGE_SECTIONS = [
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DESCRIPTION,
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DIAGRAM_VIEWER,
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.MODELS_DOCUMENTATION,
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.QUICK_START,
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DATA_ACCESS,
];
const DATA_SPACE_WIKI_PAGE_ANCHORS = DATA_PRODUCT_WIKI_PAGE_SECTIONS.map(
(activity) => generateAnchorForActivity(activity),
);
type DataProductPageNavigationCommand = {
anchor: string;
};
export class DataProductLayoutState {
readonly dataProductViewerState: DataProductViewerState;
currentNavigationZone = '';
isExpandedModeEnabled = false;
frame?: HTMLElement | undefined;
header?: HTMLElement | undefined;
isTopScrollerVisible = false;
private wikiPageAnchorIndex = new Map<string, HTMLElement>();
wikiPageNavigationCommand?: DataProductPageNavigationCommand | undefined;
private wikiPageVisibleAnchors: string[] = [];
private wikiPageScrollIntersectionObserver?: IntersectionObserver | undefined;
constructor(dataProductViewerState: DataProductViewerState) {
makeObservable<
DataProductLayoutState,
| 'wikiPageAnchorIndex'
| 'wikiPageVisibleAnchors'
| 'updatePageVisibleAnchors'
>(this, {
currentNavigationZone: observable,
isExpandedModeEnabled: observable,
isTopScrollerVisible: observable,
wikiPageAnchorIndex: observable,
wikiPageVisibleAnchors: observable,
frame: observable.ref,
wikiPageNavigationCommand: observable.ref,
isWikiPageFullyRendered: computed,
registerWikiPageScrollObserver: action,
setCurrentNavigationZone: action,
enableExpandedMode: action,
setFrame: action,
setTopScrollerVisible: action,
setWikiPageAnchor: action,
unsetWikiPageAnchor: action,
setWikiPageAnchorToNavigate: action,
updatePageVisibleAnchors: action,
});
this.dataProductViewerState = dataProductViewerState;
}
setCurrentNavigationZone(val: string): void {
this.currentNavigationZone = val;
}
get isWikiPageFullyRendered(): boolean {
return (
Boolean(this.frame) &&
DATA_PRODUCT_WIKI_PAGE_SECTIONS.includes(
this.dataProductViewerState.currentActivity,
) &&
DATA_SPACE_WIKI_PAGE_ANCHORS.every((anchor) =>
this.wikiPageAnchorIndex.has(anchor),
) &&
Array.from(this.wikiPageAnchorIndex.values()).every(isNonNullable)
);
}
registerWikiPageScrollObserver(): void {
if (this.frame && this.isWikiPageFullyRendered) {
const wikiPageIntersectionObserver = new IntersectionObserver(
(entries, observer) => {
const anchorsWithVisibilityChanged = entries
.map((entry) => {
for (const [key, element] of this.wikiPageAnchorIndex.entries()) {
if (element === entry.target) {
return { key, isIntersecting: entry.isIntersecting };
}
}
return undefined;
})
.filter(isNonNullable);
anchorsWithVisibilityChanged.forEach((entry) => {
this.updatePageVisibleAnchors(entry.key, entry.isIntersecting);
});
// NOTE: sync scroll with menu/address is quite a delicate piece of work
// as it interferes with programatic scroll operations we do elsewhere.
// This is particularly bad when we do a programatic `smooth` scroll, which
// mimic user scrolling behavior and would tangle up with this observer
// Since currently, there's no good mechanism to detect scroll end event, and as such,
// there is no good way to temporarily disable this logic while doing the programmatic
// smooth scroll as such, we avoid supporting programatic smooth scrolling for now
// See https://github.com/w3c/csswg-drafts/issues/3744
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/scrollend_event
if (
// if current navigation zone is not set, do not update zone
this.currentNavigationZone === '' ||
// if there is no visible anchors, do not update zone
!this.wikiPageVisibleAnchors.length ||
// if some of the current visible anchors match or is parent section of the current
// navigation zone, do not update zone
this.wikiPageVisibleAnchors.some(
(visibleAnchor) =>
this.currentNavigationZone === visibleAnchor ||
this.currentNavigationZone.startsWith(
`${visibleAnchor}${NAVIGATION_ZONE_SEPARATOR}`,
),
)
) {
return;
}
const anchor = at(this.wikiPageVisibleAnchors, 0);
// this.dataProductViewerState.syncZoneWithNavigation(anchor);
const anchorChunks = anchor.split(NAVIGATION_ZONE_SEPARATOR);
const activity = anchorChunks[0];
if (activity) {
this.dataProductViewerState.setCurrentActivity(
extractActivityFromAnchor(
activity,
) as DATA_PRODUCT_VIEWER_ACTIVITY_MODE,
);
}
},
{
root: this.frame,
threshold: 0.5,
},
);
Array.from(this.wikiPageAnchorIndex.values()).forEach((el) =>
wikiPageIntersectionObserver.observe(el),
);
this.wikiPageScrollIntersectionObserver = wikiPageIntersectionObserver;
}
}
unregisterWikiPageScrollObserver(): void {
this.wikiPageScrollIntersectionObserver?.disconnect();
this.wikiPageScrollIntersectionObserver = undefined;
this.wikiPageVisibleAnchors = [];
}
private updatePageVisibleAnchors(
changedAnchor: string,
isIntersecting: boolean,
): void {
if (isIntersecting) {
const anchors = this.wikiPageVisibleAnchors.filter(
(anchor) => changedAnchor !== anchor,
);
// NOTE: the newly visible anchors should be the furthest one in
// the direction of scroll
anchors.push(changedAnchor);
this.wikiPageVisibleAnchors = anchors;
} else {
this.wikiPageVisibleAnchors = this.wikiPageVisibleAnchors.filter(
(anchor) => changedAnchor !== anchor,
);
}
}
enableExpandedMode(val: boolean): void {
this.isExpandedModeEnabled = val;
}
setFrame(val: HTMLElement | undefined): void {
this.frame = val;
}
setTopScrollerVisible(val: boolean): void {
this.isTopScrollerVisible = val;
}
setWikiPageAnchor(anchorKey: string, element: HTMLElement): void {
// do not allow overriding existing anchor
if (!this.wikiPageAnchorIndex.has(anchorKey)) {
this.wikiPageAnchorIndex.set(anchorKey, element);
}
}
unsetWikiPageAnchor(anchorKey: string): void {
this.wikiPageAnchorIndex.delete(anchorKey);
}
setWikiPageAnchorToNavigate(
val: DataProductPageNavigationCommand | undefined,
): void {
this.wikiPageNavigationCommand = val;
}
navigateWikiPageAnchor(): void {
if (
this.frame &&
this.wikiPageNavigationCommand &&
this.isWikiPageFullyRendered
) {
const anchor = this.wikiPageNavigationCommand.anchor;
const matchingWikiPageSection = this.wikiPageAnchorIndex.get(anchor);
const anchorChunks = anchor.split(NAVIGATION_ZONE_SEPARATOR);
if (matchingWikiPageSection) {
this.frame.scrollTop =
matchingWikiPageSection.offsetTop -
(this.header?.getBoundingClientRect().height ?? 0);
} else if (
generateAnchorForActivity(
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DIAGRAM_VIEWER,
) === anchorChunks[0]
) {
this.frame.scrollTop =
guaranteeNonNullable(
this.wikiPageAnchorIndex.get(
generateAnchorForActivity(
DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DIAGRAM_VIEWER,
),
),
).offsetTop - (this.header?.getBoundingClientRect().height ?? 0);
// const matchingDiagram =
// this.dataProductViewerState.dataSpaceAnalysisResult.diagrams.find(
// (diagram) => generateAnchorForDiagram(diagram) === anchor,
// );
// if (matchingDiagram) {
// this.dataProductViewerState.diagramViewerState.setCurrentDiagram(
// matchingDiagram,
// );
// }
}
this.setWikiPageAnchorToNavigate(undefined);
}
}
}