Skip to content

Instantly share code, notes, and snippets.

@tntmarket
Last active July 5, 2020 01:41
Show Gist options
  • Save tntmarket/dcf014f83cacccca52516bc86e43bff4 to your computer and use it in GitHub Desktop.
Save tntmarket/dcf014f83cacccca52516bc86e43bff4 to your computer and use it in GitHub Desktop.
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