UNPKG

@finos/legend-application-marketplace

Version:
266 lines (242 loc) 9.56 kB
/** * 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); } } }