Files
Aslam 36e0e19212 Setup (#112)
* 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>
2024-08-07 20:57:22 +03:00

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