Last active
July 5, 2020 01:41
-
-
Save tntmarket/dcf014f83cacccca52516bc86e43bff4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {findLast, last} from 'lodash' | |
import {Selectors} from 'src/core/roam/selectors' | |
import {assumeExists} from 'src/core/common/assert' | |
import {BlockElement, BlockId, RoamBlock} from 'src/core/features/vim-mode/roam/roam-block' | |
import {relativeItem} from 'src/core/common/array' | |
export type PanelId = number | |
type PanelElement = HTMLElement | |
/** | |
* A "Panel" is a viewport that contains blocks. For now, there is just | |
* the Main panel and the Right panel. It is analogous a vim window | |
* | |
* In the future, each page in the right panel could be controlled as it's | |
* own "panel", which might be useful for Matsuchak/Masonry mode | |
* | |
* The generically reusable parts of this should probably move to core/roam | |
*/ | |
export class RoamPanel { | |
private focusedBlock: BlockId | null | |
constructor() { | |
this.focusedBlock = null; | |
} | |
private blocks = (): BlockElement[] => Array.from(this.element.querySelectorAll(Selectors.block)) | |
private relativeBlockId(blockId: BlockId, blocksToJump: number): BlockId { | |
const blocks = this.blocks() | |
const blockIndex = blocks.findIndex(({id}) => id === blockId) | |
return relativeItem(blocks, blockIndex, blocksToJump).id | |
} | |
selectedBlockId(): BlockId { | |
if (!this.focusedBlock || !document.getElementById(this.focusedBlock)) { | |
// Fallback to selecting the first block, | |
// if blockId is not initialized yet, or the block no longer exists | |
const firstBlockId = this.firstBlock().id | |
this.selectBlock(firstBlockId) | |
return firstBlockId | |
} | |
return this.focusedBlock | |
} | |
selectedBlock(): RoamBlock { | |
return RoamBlock.get(this.selectedBlockId()) | |
} | |
selectBlock(blockId: string) { | |
this.focusedBlock = blockId | |
this.scrollUntilBlockIsVisible(this.selectedBlock().element) | |
} | |
selectRelativeBlock(blocksToJump: number) { | |
const block = this.selectedBlock().element | |
this.selectBlock(this.relativeBlockId(block.id, blocksToJump)) | |
} | |
scrollUntilBlockIsVisible(block: BlockElement) { | |
this.scroll(blockScrollOverflow(block)) | |
} | |
get element(): PanelElement { | |
return assumeExists(document.querySelector(Selectors.sidebarContent) as HTMLElement) | |
} | |
firstBlock(): BlockElement { | |
return assumeExists(this.element.querySelector(Selectors.block) as BlockElement) | |
} | |
lastBlock(): BlockElement { | |
return assumeExists(last(this.blocks()) as BlockElement) | |
} | |
static selected(): RoamPanel { | |
return state.panels[state.focusedPanel] | |
} | |
static selectPreviousPanel() { | |
state.focusedPanel = Math.max(state.focusedPanel - 1, 0) | |
} | |
static selectNextPanel() { | |
state.focusedPanel = Math.min(state.focusedPanel + 1, state.panels.length - 1) | |
} | |
/** | |
* This should be called whenever anything in the sidebar changes, | |
* to synchronize Panel's internal state to match Roam's UI | |
*/ | |
static updateSidePanels() { | |
/** | |
* If the focused block is persisted in Panel instances, I have to either: | |
* 1. re-hydrate the focusedBlock after reconstructing panels OR | |
* 2. identify the specific panels to remove, and the right spot to insert | |
*/ | |
if (document.querySelector(Selectors.sidebarContent)) { | |
state.panels = [new RoamPanel(), new RoamSidePanel()] | |
} else { | |
state.panels = [new RoamPanel()] | |
} | |
} | |
scrollAndReselectBlockToStayVisible(scrollPx: number) { | |
this.scroll(scrollPx) | |
this.selectClosestVisibleBlock(this.selectedBlock().element) | |
} | |
private scroll(scrollPx: number) { | |
this.element.scrollTop += scrollPx | |
} | |
private selectClosestVisibleBlock(block: BlockElement) { | |
const scrollOverflow = blockScrollOverflow(block) | |
if (scrollOverflow < 0) { | |
// Block has gone out of bounds off the top | |
this.selectBlock(this.firstVisibleBlock().id) | |
} | |
if (scrollOverflow > 0) { | |
// Block has gone out of bounds off the bottom | |
this.selectBlock(this.lastVisibleBlock().id) | |
} | |
} | |
private firstVisibleBlock(): BlockElement { | |
return assumeExists(this.blocks().find(blockIsVisible), 'Could not find any visible block') | |
} | |
private lastVisibleBlock() { | |
return assumeExists(findLast(this.blocks(), blockIsVisible), 'Could not find any visible block') | |
} | |
} | |
class RoamSidePanel extends RoamPanel { | |
/** | |
* Turns out I can't use just the selector, because I need to select the parent element of | |
* Selectors.mainContent | |
*/ | |
get element(): PanelElement { | |
const articleElement = assumeExists(document.querySelector(Selectors.mainContent)) | |
return assumeExists(articleElement.parentElement) | |
} | |
} | |
type BlockNavigationState = { | |
panels: Array<RoamPanel> | |
focusedPanel: PanelId | |
} | |
const state: BlockNavigationState = { | |
panels: [new RoamPanel()], | |
focusedPanel: 0, | |
} | |
// Roughly two lines on either side | |
const SCROLL_PADDING_TOP = 100 | |
const SCROLL_PADDING_BOTTOM = 60 | |
/** | |
* If a block is: | |
* - too far above the viewport, this will be negative | |
* - too far below the viewport, this will be positive | |
* - visible, this will be 0 | |
*/ | |
const blockScrollOverflow = (block: BlockElement): number => { | |
const {top, height} = block.getBoundingClientRect() | |
const overflowTop = SCROLL_PADDING_TOP - top | |
if (overflowTop > 0) { | |
return -overflowTop | |
} | |
const overflowBottom = top + height + SCROLL_PADDING_BOTTOM - window.innerHeight | |
if (overflowBottom > 0) { | |
return overflowBottom | |
} | |
return 0 | |
} | |
const blockIsVisible = (block: BlockElement): boolean => blockScrollOverflow(block) === 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment