165 lines
3.8 KiB
TypeScript
165 lines
3.8 KiB
TypeScript
|
|
/* eslint-disable no-underscore-dangle -- Lexical nodes use protected __ fields by convention. */
|
||
|
|
|
||
|
|
import {
|
||
|
|
$applyNodeReplacement,
|
||
|
|
$getSelection,
|
||
|
|
$isRangeSelection,
|
||
|
|
TextNode,
|
||
|
|
} from "lexical"
|
||
|
|
import type {
|
||
|
|
DOMConversionMap,
|
||
|
|
DOMExportOutput,
|
||
|
|
EditorConfig,
|
||
|
|
LexicalEditor,
|
||
|
|
LexicalNode,
|
||
|
|
NodeKey,
|
||
|
|
SerializedTextNode,
|
||
|
|
Spread,
|
||
|
|
} from "lexical"
|
||
|
|
|
||
|
|
import { isISODate } from "../date-value"
|
||
|
|
|
||
|
|
export type SerializedDateNode = Spread<
|
||
|
|
{
|
||
|
|
date: string
|
||
|
|
},
|
||
|
|
SerializedTextNode
|
||
|
|
>
|
||
|
|
|
||
|
|
export class DateNode extends TextNode {
|
||
|
|
__date: string
|
||
|
|
|
||
|
|
static getType(): string {
|
||
|
|
return "date"
|
||
|
|
}
|
||
|
|
|
||
|
|
static clone(node: DateNode): DateNode {
|
||
|
|
return new DateNode(node.__date, node.__text, node.__key)
|
||
|
|
}
|
||
|
|
|
||
|
|
static importJSON(node: SerializedDateNode): DateNode {
|
||
|
|
return $createDateNode(node.date, node.text).updateFromJSON(node)
|
||
|
|
}
|
||
|
|
|
||
|
|
static importDOM(): DOMConversionMap | null {
|
||
|
|
return {
|
||
|
|
time: (element) => {
|
||
|
|
const date =
|
||
|
|
element.getAttribute("datetime") || element.dataset.lexicalDate
|
||
|
|
if (!date || !isISODate(date)) return null
|
||
|
|
|
||
|
|
return {
|
||
|
|
conversion: () => ({
|
||
|
|
node: $createDateNode(date, element.textContent || date),
|
||
|
|
forChild: () => null,
|
||
|
|
}),
|
||
|
|
priority: 4,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
constructor(date: string, text?: string, key?: NodeKey) {
|
||
|
|
super(text ?? date, key)
|
||
|
|
this.__date = date
|
||
|
|
}
|
||
|
|
|
||
|
|
createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {
|
||
|
|
const element = super.createDOM(config, editor)
|
||
|
|
element.dataset.lexicalDate = this.__date
|
||
|
|
return element
|
||
|
|
}
|
||
|
|
|
||
|
|
updateDOM(
|
||
|
|
previousNode: this,
|
||
|
|
element: HTMLElement,
|
||
|
|
config: EditorConfig
|
||
|
|
): boolean {
|
||
|
|
const didUpdate = super.updateDOM(previousNode, element, config)
|
||
|
|
if (previousNode.__date !== this.__date) {
|
||
|
|
element.dataset.lexicalDate = this.__date
|
||
|
|
}
|
||
|
|
return didUpdate
|
||
|
|
}
|
||
|
|
|
||
|
|
exportDOM(): DOMExportOutput {
|
||
|
|
const element = document.createElement("time")
|
||
|
|
element.dateTime = this.__date
|
||
|
|
element.dataset.lexicalDate = this.__date
|
||
|
|
element.textContent = this.getTextContent()
|
||
|
|
return { element }
|
||
|
|
}
|
||
|
|
|
||
|
|
exportJSON(): SerializedDateNode {
|
||
|
|
return {
|
||
|
|
...super.exportJSON(),
|
||
|
|
date: this.__date,
|
||
|
|
type: "date",
|
||
|
|
version: 1,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
getDate(): string {
|
||
|
|
return this.getLatest().__date
|
||
|
|
}
|
||
|
|
|
||
|
|
setDate(date: string, text: string): this {
|
||
|
|
const writable = this.getWritable()
|
||
|
|
writable.__date = date
|
||
|
|
writable.__text = text
|
||
|
|
return writable
|
||
|
|
}
|
||
|
|
|
||
|
|
isTextEntity(): true {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function $createDateNode(date: string, text?: string): DateNode {
|
||
|
|
return $applyNodeReplacement(new DateNode(date, text))
|
||
|
|
.setMode("token")
|
||
|
|
.setDetail("unmergable")
|
||
|
|
}
|
||
|
|
|
||
|
|
export function $isDateNode(
|
||
|
|
node: LexicalNode | null | undefined
|
||
|
|
): node is DateNode {
|
||
|
|
return node instanceof DateNode
|
||
|
|
}
|
||
|
|
|
||
|
|
export function $getSelectedDateNode(): DateNode | null {
|
||
|
|
const selection = $getSelection()
|
||
|
|
if (!$isRangeSelection(selection)) return null
|
||
|
|
|
||
|
|
const anchorNode = selection.anchor.getNode()
|
||
|
|
const focusNode = selection.focus.getNode()
|
||
|
|
if (anchorNode === focusNode && $isDateNode(anchorNode)) return anchorNode
|
||
|
|
|
||
|
|
const selectedNodes = selection.getNodes()
|
||
|
|
return selectedNodes.length === 1 && $isDateNode(selectedNodes[0])
|
||
|
|
? selectedNodes[0]
|
||
|
|
: null
|
||
|
|
}
|
||
|
|
|
||
|
|
export function $insertOrUpdateDate(
|
||
|
|
date: string,
|
||
|
|
text: string
|
||
|
|
): DateNode | null {
|
||
|
|
const selectedDateNode = $getSelectedDateNode()
|
||
|
|
|
||
|
|
if (selectedDateNode) {
|
||
|
|
selectedDateNode.setDate(date, text).select(0, text.length)
|
||
|
|
return selectedDateNode
|
||
|
|
}
|
||
|
|
|
||
|
|
const selection = $getSelection()
|
||
|
|
if (!$isRangeSelection(selection)) return null
|
||
|
|
|
||
|
|
const dateNode = $createDateNode(date, text)
|
||
|
|
selection.insertNodes([dateNode])
|
||
|
|
dateNode.select(0, text.length)
|
||
|
|
return dateNode
|
||
|
|
}
|
||
|
|
|
||
|
|
export { isISODate } from "../date-value"
|