mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
* wip * wip * wip3 * chore: utils * feat: add command * wip * fix: key duplicate * fix: move and check * fix: use react-use instead * fix: sidebar * chore: make dynamic * chore: tablet mode * chore: fix padding * chore: link instead of inbox * fix: use dnd kit * feat: add select component * chore: use atom * refactor: remove dnd provider * feat: disabled drag when sort is not manual * search route * . * feat: accessibility * fix: search * . * . * . * fix: sidebar collapsed * ai search layout * . * . * . * . * ai responsible content * . * . * . * . * . * global topic route * global topic correct route * topic buttons * sidebar search navigation * ai * Update jazz * . * . * . * . * . * learning status * . * . * chore: content header * fix: pointer none when dragging. prevent auto click after drag end * fix: confirm * fix: prevent drag when editing * chore: remove unused fn * fix: check propagation * chore: list * chore: tweak sonner * chore: update stuff * feat: add badge * chore: close edit when create * chore: escape on manage form * refactor: remove learn path * css: responsive item * chore: separate pages and topic * reafactor: remove new-schema * feat(types): extend jazz type so it can be nullable * chore: use new types * fix: missing deps * fix: link * fix: sidebar in layout * fix: quotes * css: use medium instead semi * Actual streaming and rendering markdown response * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * . * chore: metadata * feat: la-editor * . * fix: editor and page * . * . * . * . * . * . * fix: remove link * chore: page sidebar * fix: remove 'replace with learning status' * fix: link * fix: link * chore: update schema * chore: use new schema * fix: instead of showing error, just do unique slug * feat: create slug * refactor apply * update package json * fix: schema personal page * chore: editor * feat: pages * fix: metadata * fix: jazz provider * feat: handling data * feat: page detail * chore: server page to id * chore: use id instead of slug * chore: update content header * chore: update link header implementation * refactor: global.css * fix: la editor use animation frame * fix: editor export ref * refactor: page detail * chore: tidy up schema * chore: adapt to new schema * fix: wrap using settimeout * fix: wrap using settimeout * . * . --------- Co-authored-by: marshennikovaolga <marshennikova@gmail.com> Co-authored-by: Nikita <github@nikiv.dev> Co-authored-by: Anselm <anselm.eickhoff@gmail.com> Co-authored-by: Damian Tarnawski <gthetarnav@gmail.com>
235 lines
5.8 KiB
TypeScript
235 lines
5.8 KiB
TypeScript
import { Editor, Extension } from "@tiptap/core"
|
|
import { ReactRenderer } from "@tiptap/react"
|
|
import Suggestion, { SuggestionProps, SuggestionKeyDownProps } from "@tiptap/suggestion"
|
|
import { PluginKey } from "@tiptap/pm/state"
|
|
import tippy from "tippy.js"
|
|
|
|
import { GROUPS } from "./groups"
|
|
import { MenuList } from "./menu-list"
|
|
|
|
const EXTENSION_NAME = "slashCommand"
|
|
|
|
let popup: any
|
|
|
|
export const SlashCommand = Extension.create({
|
|
name: EXTENSION_NAME,
|
|
priority: 200,
|
|
|
|
onCreate() {
|
|
popup = tippy("body", {
|
|
interactive: true,
|
|
trigger: "manual",
|
|
placement: "bottom-start",
|
|
theme: "slash-command",
|
|
maxWidth: "16rem",
|
|
offset: [16, 8],
|
|
popperOptions: {
|
|
strategy: "fixed",
|
|
modifiers: [{ name: "flip", enabled: false }]
|
|
}
|
|
})
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
return [
|
|
Suggestion({
|
|
editor: this.editor,
|
|
char: "/",
|
|
allowSpaces: true,
|
|
startOfLine: true,
|
|
pluginKey: new PluginKey(EXTENSION_NAME),
|
|
|
|
allow: ({ state, range }) => {
|
|
const $from = state.doc.resolve(range.from)
|
|
const isRootDepth = $from.depth === 1
|
|
const isParagraph = $from.parent.type.name === "paragraph"
|
|
const isStartOfNode = $from.parent.textContent?.charAt(0) === "/"
|
|
const isInColumn = this.editor.isActive("column")
|
|
|
|
const afterContent = $from.parent.textContent?.substring($from.parent.textContent?.indexOf("/"))
|
|
const isValidAfterContent = !afterContent?.endsWith(" ")
|
|
|
|
return (
|
|
((isRootDepth && isParagraph && isStartOfNode) || (isInColumn && isParagraph && isStartOfNode)) &&
|
|
isValidAfterContent
|
|
)
|
|
},
|
|
|
|
command: ({ editor, props }: { editor: Editor; props: any }) => {
|
|
const { view, state } = editor
|
|
const { $head, $from } = view.state.selection
|
|
|
|
const end = $from.pos
|
|
const from = $head?.nodeBefore
|
|
? end - ($head.nodeBefore.text?.substring($head.nodeBefore.text?.indexOf("/")).length ?? 0)
|
|
: $from.start()
|
|
|
|
const tr = state.tr.deleteRange(from, end)
|
|
view.dispatch(tr)
|
|
|
|
props.action(editor)
|
|
view.focus()
|
|
},
|
|
|
|
items: ({ query }: { query: string }) => {
|
|
return GROUPS.map(group => ({
|
|
...group,
|
|
commands: group.commands
|
|
.filter(item => {
|
|
const labelNormalized = item.label.toLowerCase().trim()
|
|
const queryNormalized = query.toLowerCase().trim()
|
|
|
|
if (item.aliases) {
|
|
const aliases = item.aliases.map(alias => alias.toLowerCase().trim())
|
|
return labelNormalized.includes(queryNormalized) || aliases.includes(queryNormalized)
|
|
}
|
|
|
|
return labelNormalized.includes(queryNormalized)
|
|
})
|
|
.filter(command => (command.shouldBeHidden ? !command.shouldBeHidden(this.editor) : true))
|
|
.map(command => ({
|
|
...command,
|
|
isEnabled: true
|
|
}))
|
|
})).filter(group => group.commands.length > 0)
|
|
},
|
|
|
|
render: () => {
|
|
let component: any
|
|
let scrollHandler: (() => void) | null = null
|
|
|
|
return {
|
|
onStart: (props: SuggestionProps) => {
|
|
component = new ReactRenderer(MenuList, {
|
|
props,
|
|
editor: props.editor
|
|
})
|
|
|
|
const { view } = props.editor
|
|
const editorNode = view.dom as HTMLElement
|
|
|
|
const getReferenceClientRect = () => {
|
|
if (!props.clientRect) {
|
|
return props.editor.storage[EXTENSION_NAME].rect
|
|
}
|
|
|
|
const rect = props.clientRect()
|
|
|
|
if (!rect) {
|
|
return props.editor.storage[EXTENSION_NAME].rect
|
|
}
|
|
|
|
let yPos = rect.y
|
|
|
|
if (rect.top + component.element.offsetHeight + 40 > window.innerHeight) {
|
|
const diff = rect.top + component.element.offsetHeight - window.innerHeight + 40
|
|
yPos = rect.y - diff
|
|
}
|
|
|
|
const editorXOffset = editorNode.getBoundingClientRect().x
|
|
return new DOMRect(rect.x, yPos, rect.width, rect.height)
|
|
}
|
|
|
|
scrollHandler = () => {
|
|
popup?.[0].setProps({
|
|
getReferenceClientRect
|
|
})
|
|
}
|
|
|
|
view.dom.parentElement?.addEventListener("scroll", scrollHandler)
|
|
|
|
popup?.[0].setProps({
|
|
getReferenceClientRect,
|
|
appendTo: () => document.body,
|
|
content: component.element
|
|
})
|
|
|
|
popup?.[0].show()
|
|
},
|
|
|
|
onUpdate(props: SuggestionProps) {
|
|
component.updateProps(props)
|
|
|
|
const { view } = props.editor
|
|
const editorNode = view.dom as HTMLElement
|
|
|
|
const getReferenceClientRect = () => {
|
|
if (!props.clientRect) {
|
|
return props.editor.storage[EXTENSION_NAME].rect
|
|
}
|
|
|
|
const rect = props.clientRect()
|
|
|
|
if (!rect) {
|
|
return props.editor.storage[EXTENSION_NAME].rect
|
|
}
|
|
|
|
return new DOMRect(rect.x, rect.y, rect.width, rect.height)
|
|
}
|
|
|
|
let scrollHandler = () => {
|
|
popup?.[0].setProps({
|
|
getReferenceClientRect
|
|
})
|
|
}
|
|
|
|
view.dom.parentElement?.addEventListener("scroll", scrollHandler)
|
|
|
|
props.editor.storage[EXTENSION_NAME].rect = props.clientRect
|
|
? getReferenceClientRect()
|
|
: {
|
|
width: 0,
|
|
height: 0,
|
|
left: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
}
|
|
popup?.[0].setProps({
|
|
getReferenceClientRect
|
|
})
|
|
},
|
|
|
|
onKeyDown(props: SuggestionKeyDownProps) {
|
|
if (props.event.key === "Escape") {
|
|
popup?.[0].hide()
|
|
return true
|
|
}
|
|
|
|
if (!popup?.[0].state.isShown) {
|
|
popup?.[0].show()
|
|
}
|
|
|
|
return component.ref?.onKeyDown(props)
|
|
},
|
|
|
|
onExit(props) {
|
|
popup?.[0].hide()
|
|
if (scrollHandler) {
|
|
const { view } = props.editor
|
|
view.dom.parentElement?.removeEventListener("scroll", scrollHandler)
|
|
}
|
|
component.destroy()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
]
|
|
},
|
|
|
|
addStorage() {
|
|
return {
|
|
rect: {
|
|
width: 0,
|
|
height: 0,
|
|
left: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
export default SlashCommand
|