Before repair nav components
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 4s
This commit is contained in:
145
components/Editor.js
Normal file
145
components/Editor.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import { h, $ } from "sigpro"
|
||||
import { get, cls, isFn } from "./All.js"
|
||||
|
||||
export const Editor = (p) => {
|
||||
const { value, class: extraClass } = p
|
||||
let editorRef = null
|
||||
|
||||
const isSource = $(false)
|
||||
const source = $("")
|
||||
|
||||
const notify = () => {
|
||||
if (!editorRef) return
|
||||
const html = editorRef.innerHTML
|
||||
if (isFn(value)) value(html)
|
||||
else p.onchange?.(html)
|
||||
}
|
||||
|
||||
const exec = (cmd, val = null) => {
|
||||
if (!editorRef) return
|
||||
editorRef.focus()
|
||||
document.execCommand(cmd, false, val)
|
||||
notify()
|
||||
}
|
||||
|
||||
const queryState = (cmd, val = null) => {
|
||||
if (!editorRef) return false
|
||||
try {
|
||||
if (cmd === 'formatBlock') {
|
||||
const sel = window.getSelection()
|
||||
if (!sel.rangeCount) return false
|
||||
let node = sel.getRangeAt(0).commonAncestorContainer
|
||||
while (node && node !== editorRef) {
|
||||
if (node.nodeType === 1 && node.tagName === val) return true
|
||||
node = node.parentNode
|
||||
}
|
||||
return false
|
||||
}
|
||||
return document.queryCommandState(cmd)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const toolbar = h("div", { class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200" }, [
|
||||
h("div", { class: "flex flex-wrap gap-1 flex-1" }, [
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${queryState('bold') ? 'btn-active' : ''}`,
|
||||
onclick: () => exec("bold")
|
||||
}, h("span", { class: "icon-[lucide--bold]" })),
|
||||
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${queryState('italic') ? 'btn-active' : ''}`,
|
||||
onclick: () => exec("italic")
|
||||
}, h("span", { class: "icon-[lucide--italic]" })),
|
||||
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${queryState('underline') ? 'btn-active' : ''}`,
|
||||
onclick: () => exec("underline")
|
||||
}, h("span", { class: "icon-[lucide--underline]" })),
|
||||
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${queryState('strikeThrough') ? 'btn-active' : ''}`,
|
||||
onclick: () => exec("strikeThrough")
|
||||
}, h("span", { class: "icon-[lucide--strikethrough]" })),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertUnorderedList") }, h("span", { class: "icon-[lucide--list]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("insertOrderedList") }, h("span", { class: "icon-[lucide--list-ordered]" })),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${queryState('formatBlock', 'BLOCKQUOTE') ? 'btn-active' : ''}`,
|
||||
onclick: () => exec("formatBlock", queryState('formatBlock', 'BLOCKQUOTE') ? 'P' : 'BLOCKQUOTE')
|
||||
}, h("span", { class: "icon-[lucide--quote]" })),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
h("select", { class: "select select-xs w-16", onchange: (e) => exec("fontSize", e.target.value), value: "3" }, [
|
||||
h("option", { value: "1" }, "1"),
|
||||
h("option", { value: "2" }, "2"),
|
||||
h("option", { value: "3" }, "3"),
|
||||
h("option", { value: "4" }, "4"),
|
||||
h("option", { value: "5" }, "5"),
|
||||
h("option", { value: "6" }, "6"),
|
||||
h("option", { value: "7" }, "7"),
|
||||
]),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("undo") }, h("span", { class: "icon-[lucide--undo-2]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("redo") }, h("span", { class: "icon-[lucide--redo-2]" })),
|
||||
]),
|
||||
|
||||
h("div", { class: "flex" }, [
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: () => `btn btn-ghost btn-xs ${isSource() ? 'btn-active' : ''}`,
|
||||
onclick: () => {
|
||||
const wasSource = isSource()
|
||||
if (!wasSource) {
|
||||
source(editorRef?.innerHTML || "")
|
||||
} else {
|
||||
if (editorRef) {
|
||||
editorRef.innerHTML = source()
|
||||
notify()
|
||||
}
|
||||
}
|
||||
isSource(!wasSource)
|
||||
}
|
||||
}, h("span", { class: "icon-[lucide--code-2]" }))
|
||||
])
|
||||
])
|
||||
|
||||
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden", extraClass) }, [
|
||||
toolbar,
|
||||
h("div", { class: "relative" }, [
|
||||
h("div", {
|
||||
ref: el => {
|
||||
if (!editorRef && el) {
|
||||
editorRef = el
|
||||
el.innerHTML = get(value) || ""
|
||||
}
|
||||
},
|
||||
style: () => `min-height:10rem;${isSource() ? 'display:none' : ''}`,
|
||||
class: "p-3 outline-none text-base-content [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:list-item [&_p]:m-0 [&_div]:m-0 [&_br]:content-[''] [&_br]:block [&_br]:h-[1em]",
|
||||
contenteditable: "true",
|
||||
oninput: notify,
|
||||
onpaste: () => setTimeout(notify, 0)
|
||||
}),
|
||||
h("textarea", {
|
||||
class: "w-full min-h-[10rem] p-3 outline-none font-mono text-sm bg-base-200 border-0",
|
||||
style: () => isSource() ? '' : 'display:none',
|
||||
value: source,
|
||||
oninput: (e) => source(e.target.value)
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user