Skip to content

Commit

Permalink
fix: can't focus on cell after insert row
Browse files Browse the repository at this point in the history
  • Loading branch information
Z233 committed Aug 19, 2023
1 parent 9aaf893 commit 18b5e20
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 61 deletions.
18 changes: 15 additions & 3 deletions src/editor/md-table-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@tgrosinger/md-advanced-tables'
import { MarkdownView, type App, type TFile } from 'obsidian'
import type { Maybe } from 'src/types'
import { debounceRAF } from 'src/util/helper'
import { debounceRAFPromise } from 'src/util/helper'
import { ObsidianTextEditor } from './obsidian-text-editor'

type MdTableEditorOptions = {
Expand All @@ -33,6 +33,7 @@ export class MdTableEditor {
private _table: Table
private _startRow: number
private _endRow: number
private _isApplying = false

constructor({ app, file, table, startRow, endRow }: MdTableEditorOptions) {
this._app = app
Expand Down Expand Up @@ -92,17 +93,28 @@ export class MdTableEditor {
return this._focusState
}

applyChanges = debounceRAF(this._applyChangesOrigin.bind(this))
get isApplying() {
return this._isApplying
}

applyChanges = debounceRAFPromise(this._applyChangesOrigin.bind(this) as typeof this._applyChangesOrigin)

private _applyChangesOrigin() {
this._isApplying = true

this._ensureEditorLoaded()

if (!this._mte) throw new Error('No active editor')
if (!this._mte) {
this._isApplying = false
throw new Error('No active editor')
}

const formatted = formatTable(this._table, defaultOptions)
const newLines = formatted.table.toLines()

this._mte._updateLines(this._startRow, this._endRow + 1, newLines)

this._isApplying = false
}

private _ensureEditorLoaded() {
Expand Down
13 changes: 8 additions & 5 deletions src/ui/plan/Plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useState,
useSyncExternalStore,
type FC,
memo,
} from 'preact/compat'
import { PlanTable } from './PlanTable'
import { MdTableEditor } from 'src/editor/md-table-editor'
Expand Down Expand Up @@ -35,6 +36,11 @@ type MteLoader = ({
endRow: number
}) => MdTableEditor

const MemorizedPlanTable = memo(
PlanTable,
(prev, next) => prev.data.every((d, i) => shallowCompare(d, next.data[i]))
)

const Plan: FC<{
app: App
settings: SuperPlanSettings
Expand Down Expand Up @@ -92,7 +98,7 @@ const Plan: FC<{

return (
<PlanProvider mte={mte} app={app} settings={settings}>
<PlanTable data={scheduledData} />
<MemorizedPlanTable data={scheduledData} />
</PlanProvider>
)
}
Expand Down Expand Up @@ -122,8 +128,5 @@ export const renderPlan = ({
const mteLoader: MteLoader = ({ table, startRow, endRow }) =>
new MdTableEditor({ app, file, table, startRow, endRow })

render(
<Plan app={app} sync={sync} mteLoader={mteLoader} settings={settings} />,
container
)
render(<Plan app={app} sync={sync} mteLoader={mteLoader} settings={settings} />, container)
}
64 changes: 49 additions & 15 deletions src/ui/plan/PlanTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'
import { useEffect, useState, type FC, createElement, useRef, useLayoutEffect } from 'preact/compat'
import {
useEffect,
useState,
type FC,
createElement,
useRef,
useLayoutEffect,
useCallback,
} from 'preact/compat'
import type { JSXInternal } from 'preact/src/jsx'
import { ColumnKeys, ColumnKeysMap, Columns } from 'src/constants'
import { Events, GlobalMediator } from 'src/mediator'
Expand All @@ -19,7 +27,6 @@ import { focusStyle, indexCellStyle } from './styles'
import { TableRow } from './TableRow'
import type { CellPosition, PlanTableColumnDef } from './types'


export const tableColumns: PlanTableColumnDef[] = [
{
header: 'F',
Expand Down Expand Up @@ -53,12 +60,15 @@ export const tableColumns: PlanTableColumnDef[] = [
},
]

export const PlanTable: FC<{ data: PlanData }> = (props) => {
type PlanTableProps = { data: PlanData }

export const PlanTable: FC<PlanTableProps> = (props) => {
const { data } = props
const { insertRowBelow } = usePlan()

const [highlightedCell, setHighlightedCell] = useState<Maybe<CellPosition>>()
const focusableElementsRef = useRef<Map<string, HTMLInputElement>>(new Map())
const isPatchingRef = useRef(false)

const updateFocusableElement = (position: CellPosition, element: Maybe<HTMLInputElement>) => {
const { rowIndex, columnKey } = position
Expand All @@ -70,15 +80,28 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {
}
}

if (highlightedCell) {
const { rowIndex, columnKey } = highlightedCell
setImmediate(() => {
const focusStartTimeStampRef = useRef(0)

const focusOnHighlightedCell = useCallback(() => {
if (highlightedCell) {
const { rowIndex, columnKey } = highlightedCell
const el = focusableElementsRef.current.get(`${rowIndex}-${columnKey}`)
if (el) {
el.focus()
setImmediate(() => {
el.focus()
focusStartTimeStampRef.current = performance.now()
})
}
})
}
}
}, [highlightedCell])

useLayoutEffect(() => {
if (highlightedCell) focusOnHighlightedCell()

return () => {
focusStartTimeStampRef.current = 0
}
}, [highlightedCell])

const [highlightedRowId, setHighlightedRowId] = useState('')

Expand All @@ -89,7 +112,9 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {
})

const handleCellFocus = (rowIndex: number, columnKey: ColumnKeys) => {
setHighlightedCell({ rowIndex, columnKey })
if (highlightedCell?.rowIndex !== rowIndex || highlightedCell?.columnKey !== columnKey) {
setHighlightedCell({ rowIndex, columnKey })
}
}

const handleCellMouseDown: JSXInternal.MouseEventHandler<HTMLTableCellElement> = (e) => {
Expand Down Expand Up @@ -121,8 +146,7 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {

const isLastRow = nextRowIndex === tableHeight - 1
if (isLastColumn && isLastRow) {
insertRowBelow(rowIndex)
Promise.resolve().then(() => {
insertRowBelow(rowIndex).then(() => {
setHighlightedCell({ rowIndex: nextRowIndex, columnKey: ColumnKeysMap[nextColumn] })
})
return
Expand All @@ -131,16 +155,25 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {
setHighlightedCell({ rowIndex: nextRowIndex, columnKey: ColumnKeysMap[nextColumn] })
}
}

const handleBlur: JSXInternal.FocusEventHandler<HTMLElement> = (e) => {
e.stopPropagation()

if (e.timeStamp - focusStartTimeStampRef.current < 100 && highlightedCell) {
focusOnHighlightedCell()
return false
}

highlightedRowId && setHighlightedRowId('')

const relatedTarget = e.relatedTarget as Maybe<HTMLElement>
const isWithinTable = Boolean(
relatedTarget && relatedTarget.matchParent('[data-row][data-column]')
)

!isWithinTable && setHighlightedCell(null)
!isWithinTable && !isPatchingRef.current && setHighlightedCell(null)

return false
}

const tableWrapperRef = useRef<HTMLTableElement>(null)
Expand Down Expand Up @@ -183,7 +216,7 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {
parentHeight={tableWrapperInfo?.height ?? 0}
width={tableWrapperInfo?.width ?? 0}
/>
<table ref={tableWrapperRef} className="relative" onBlur={handleBlur}>
<table ref={tableWrapperRef} className="relative">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="![&>*:nth-child(2)]:border-l-0">
Expand Down Expand Up @@ -218,6 +251,7 @@ export const PlanTable: FC<{ data: PlanData }> = (props) => {
data-column={cell.column.id}
onMouseDown={handleCellMouseDown}
onKeyDown={(e) => handleCellKeyDown(e, row.index, cell.column.id as ColumnKeys)}
onBlur={handleBlur}
onFocus={() => handleCellFocus(row.index, cell.column.id as ColumnKeys)}
className={isFocused ? focusStyle : ''}
>
Expand Down
88 changes: 53 additions & 35 deletions src/ui/plan/context.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TableCell, TableRow } from '@tgrosinger/md-advanced-tables'
import { nanoid } from 'nanoid'
import type { App } from 'obsidian'
import { createContext, useContext, useRef, type FC } from 'preact/compat'
import { createContext, useContext, useRef, type FC, useCallback } from 'preact/compat'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { ColumnKeys, ColumnKeysMap, Columns } from 'src/constants'
Expand Down Expand Up @@ -50,56 +50,74 @@ export function usePlanContext() {
return context
}

type PlanActionReturnType = ReturnType<MdTableEditor['applyChanges']>

type PlanActions = {
updateCell: (row: number, columnKey: ColumnKeys, value: string) => void
deleteRow: (row: number) => void
insertRowBelow: (row: number) => void
moveRow: (from: number, to: number) => void
duplicateRow: (row: number) => void
updateCell: (row: number, columnKey: ColumnKeys, value: string) => PlanActionReturnType
deleteRow: (row: number) => PlanActionReturnType
insertRowBelow: (row: number) => PlanActionReturnType
moveRow: (from: number, to: number) => PlanActionReturnType
duplicateRow: (row: number) => PlanActionReturnType
}

export function usePlan() {
const { mte } = usePlanContext()

const updateCell: PlanActions['updateCell'] = (row, columnKey, value) => {
mte.setCellAt(row, ColumnKeysMap[columnKey], value)
mte.applyChanges()
}
const updateCell: PlanActions['updateCell'] = useCallback(
(row, columnKey, value) => {
mte.setCellAt(row, ColumnKeysMap[columnKey], value)
return mte.applyChanges()
},
[mte]
)

const deleteRow: PlanActions['deleteRow'] = (row) => {
mte.deleteRow(row)
mte.applyChanges()
}
const deleteRow: PlanActions['deleteRow'] = useCallback(
(row) => {
mte.deleteRow(row)
return mte.applyChanges()
},
[mte]
)

const insertRowBelow: PlanActions['insertRowBelow'] = (row) => {
const cells = Array.from({ length: mte.getHeaderWidth() }, (_, i) =>
i === 0 ? new TableCell(generateId()) : new TableCell(' ')
)
const tableRow = new TableRow(cells, '', '')
mte.insertRow(tableRow, row + 1)
mte.applyChanges()
}
const insertRowBelow: PlanActions['insertRowBelow'] = useCallback(
(row) => {
const cells = Array.from({ length: mte.getHeaderWidth() }, (_, i) =>
i === 0 ? new TableCell(generateId()) : new TableCell(' ')
)
const tableRow = new TableRow(cells, '', '')
mte.insertRow(tableRow, row + 1)
return mte.applyChanges()
},
[mte]
)

const moveRow: PlanActions['moveRow'] = (from, to) => {
mte.moveRow(from, to)
mte.applyChanges()
}
const moveRow: PlanActions['moveRow'] = useCallback(
(from, to) => {
mte.moveRow(from, to)
return mte.applyChanges()
},
[mte]
)

const duplicateRow: PlanActions['duplicateRow'] = (row) => {
const targetRow = mte.getRow(row + 2)
const cells = targetRow
.getCells()
.map((cell, index) => new TableCell(index === Columns.ID ? nanoid(6) : cell.content))
const duplicatedRow = new TableRow(cells, '', '')
mte.insertRow(duplicatedRow, row + 1)
mte.applyChanges()
}
const duplicateRow: PlanActions['duplicateRow'] = useCallback(
(row) => {
const targetRow = mte.getRow(row + 2)
const cells = targetRow
.getCells()
.map((cell, index) => new TableCell(index === Columns.ID ? nanoid(6) : cell.content))
const duplicatedRow = new TableRow(cells, '', '')
mte.insertRow(duplicatedRow, row + 1)
return mte.applyChanges()
},
[mte]
)

return {
updateCell,
deleteRow,
insertRowBelow,
moveRow,
duplicateRow,
isApplying: mte.isApplying,
}
}
26 changes: 23 additions & 3 deletions src/util/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,39 @@ export function transformTable(table: Table): Activity[] {

export const checkIsDataviewEnabled = () => !!getAPI()

export function debounceRAF(cb: (...args: any[]) => any) {
export function debounceRAFPromise<T extends (...args: unknown[]) => unknown, TArgs extends unknown[] = Parameters<T>, TReturn = ReturnType<T>>(fn: T) {
let rAFId: number | null = null
let deferred: {
promise: Promise<TReturn>
resolve: (value: unknown) => void
reject: (reason?: unknown) => void
} | null = null

return (...args: any[]) => {
return (...args: TArgs) => {
const context = this

if (rAFId) {
cancelAnimationFrame(rAFId)
}

if (!deferred) {
deferred = {
promise: null as unknown as Promise<TReturn>,
resolve: null as unknown as (value: unknown) => void,
reject: null as unknown as (reason?: unknown) => void,
}
deferred.promise = new Promise((resolve, reject) => {
deferred!.resolve = resolve
deferred!.reject = reject
})
}

rAFId = requestAnimationFrame(() => {
cb.apply(context, args)
Promise.resolve(fn.apply(context, args)).then(deferred?.resolve, deferred?.reject)
deferred = null
})

return deferred.promise
}
}

Expand Down

0 comments on commit 18b5e20

Please sign in to comment.