update editor and components
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
This commit is contained in:
@@ -4,31 +4,82 @@ import { get, cls, isFn } from "./All.js"
|
||||
export const Editor = (p) => {
|
||||
const { value, class: extraClass } = p
|
||||
let editorRef = null
|
||||
let savedRange = null
|
||||
|
||||
const isSource = $(false)
|
||||
const source = $("")
|
||||
const count = $(0)
|
||||
const refreshTick = $(0)
|
||||
const showEmojis = $(false)
|
||||
|
||||
const emojis = ["😀", "😊", "😉", "🧐", "😮", "🤔", "😅", "😂", "😍", "😘", "🥰", "👍", "👎", "👌", "🤝", "🤞", "👋", "👏", "🙌", "🙏", "💪", "☝️", "👇", "👈", "👉", "🖕", "✅", "⚠️", "🚀", "📢", "✉️", "❤️"]
|
||||
|
||||
const saveSelection = () => {
|
||||
const sel = window.getSelection()
|
||||
if (sel.getRangeAt && sel.rangeCount) savedRange = sel.getRangeAt(0)
|
||||
}
|
||||
|
||||
const restoreSelection = () => {
|
||||
if (savedRange) {
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(savedRange)
|
||||
}
|
||||
}
|
||||
|
||||
const triggerRefresh = () => {
|
||||
refreshTick(refreshTick() + 1)
|
||||
if (editorRef) count(editorRef.innerText.length)
|
||||
}
|
||||
|
||||
const notify = () => {
|
||||
if (!editorRef) return
|
||||
const html = editorRef.innerHTML
|
||||
if (isFn(value)) value(html)
|
||||
else p.onchange?.(html)
|
||||
triggerRefresh()
|
||||
}
|
||||
|
||||
const exec = (cmd, val = null) => {
|
||||
if (!editorRef) return
|
||||
editorRef.focus()
|
||||
if (savedRange) restoreSelection()
|
||||
document.execCommand(cmd, false, val)
|
||||
savedRange = null
|
||||
notify()
|
||||
}
|
||||
|
||||
const openLightbox = (src) => {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.style = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;`
|
||||
const img = document.createElement('img')
|
||||
img.src = src
|
||||
img.style = `max-width:95%;max-height:95%;box-shadow:0 0 30px rgba(0,0,0,0.5);border-radius:4px;`
|
||||
overlay.onclick = () => document.body.removeChild(overlay)
|
||||
overlay.appendChild(img)
|
||||
document.body.appendChild(overlay)
|
||||
}
|
||||
|
||||
const handleUpload = (file) => {
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (re) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const imgHtml = `<div style="display:inline-block; resize:both; overflow:hidden; vertical-align:bottom; line-height:0; width:200px; height:auto; border:1px dashed #ccc; padding:2px; cursor:pointer;" class="resizable-img-container"><img src="${re.target.result}" style="width:100%; height:100%; object-fit:contain; pointer-events:none;"></div> `
|
||||
exec("insertHTML", imgHtml)
|
||||
} else {
|
||||
const linkHtml = `<a href="${re.target.result}" download="${file.name}" contenteditable="false" style="display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; text-decoration:none; color:#333; font-size:12px; margin:2px; cursor:pointer;"><span class="icon-[lucide--paperclip] w-3 h-3"></span>${file.name}</a> `
|
||||
exec("insertHTML", linkHtml)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const queryState = (cmd, val = null) => {
|
||||
if (!editorRef) return false
|
||||
refreshTick(); if (!editorRef || isSource()) return false
|
||||
try {
|
||||
if (cmd === 'formatBlock') {
|
||||
const sel = window.getSelection()
|
||||
if (!sel.rangeCount) return false
|
||||
let node = sel.getRangeAt(0).commonAncestorContainer
|
||||
let node = window.getSelection().getRangeAt(0).commonAncestorContainer
|
||||
while (node && node !== editorRef) {
|
||||
if (node.nodeType === 1 && node.tagName === val) return true
|
||||
node = node.parentNode
|
||||
@@ -36,110 +87,122 @@ export const Editor = (p) => {
|
||||
return false
|
||||
}
|
||||
return document.queryCommandState(cmd)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
} 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]" })),
|
||||
const toolbar = h("div", { class: "flex flex-wrap items-center gap-1 p-2 border-b border-base-300 bg-base-200 sticky top-0 z-20" }, [
|
||||
h("div", { class: "flex flex-wrap gap-1 flex-1 items-center" }, [
|
||||
// GRUPO 1: ESTILOS
|
||||
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('bold') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("bold") }, h("span", { class: "icon-[lucide--bold]" })),
|
||||
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('italic') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("italic") }, h("span", { class: "icon-[lucide--italic]" })),
|
||||
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${queryState('underline') ? 'btn-active bg-primary/20' : ''}`, onclick: () => exec("underline") }, h("span", { class: "icon-[lucide--underline]" })),
|
||||
h("input", { type: "color", class: "w-5 h-5 p-0 border-0 bg-transparent cursor-pointer", oninput: (e) => exec("foreColor", e.target.value) }),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn btn-ghost btn-xs",
|
||||
onclick: () => exec("justifyLeft")
|
||||
}, h("span", { class: "icon-[lucide--align-left]" })),
|
||||
|
||||
// --- Botón Alineación Centro ---
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn btn-ghost btn-xs",
|
||||
onclick: () => exec("justifyCenter")
|
||||
}, h("span", { class: "icon-[lucide--align-center]" })),
|
||||
|
||||
// --- Botón Alineación Derecha ---
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn btn-ghost btn-xs",
|
||||
onclick: () => exec("justifyRight")
|
||||
}, h("span", { class: "icon-[lucide--align-right]" })),
|
||||
|
||||
h("span", { class: "w-px h-5 bg-base-300 mx-1" }),
|
||||
|
||||
// GRUPO 2: LISTAS Y PÁRRAFO
|
||||
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("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("outdent"), title: "Mover izquierda" }, h("span", { class: "icon-[lucide--indent-decrease]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("indent"), title: "Mover derecha (Tab)" }, h("span", { class: "icon-[lucide--indent-increase]" })),
|
||||
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("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]" })),
|
||||
// GRUPO 3: INSERTAR
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const url = window.prompt('URL:'); if (url) exec("createLink", url) } }, h("span", { class: "icon-[lucide--link]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => { const input = document.createElement('input'); input.type = 'file'; input.onchange = (e) => handleUpload(e.target.files[0]); input.click(); } }, h("span", { class: "icon-[lucide--paperclip]" })),
|
||||
|
||||
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"),
|
||||
// EMOJIS
|
||||
h("div", { class: "relative" }, [
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: (e) => { e.stopPropagation(); saveSelection(); showEmojis(!showEmojis()); } }, h("span", { class: "icon-[lucide--smile]" })),
|
||||
h("div", { class: "absolute top-full left-0 mt-1 p-2 bg-base-100 border border-base-300 shadow-xl rounded-box w-52 z-50 flex flex-wrap gap-1", style: () => showEmojis() ? "display:flex" : "display:none" }, emojis.map(emo => h("span", { class: "cursor-pointer hover:bg-base-200 p-1 rounded text-lg", onclick: (e) => { e.stopPropagation(); exec("insertText", emo); showEmojis(false); } }, emo)))
|
||||
]),
|
||||
|
||||
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]" })),
|
||||
// GRUPO 4: UTILIDADES
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("removeFormat") }, h("span", { class: "icon-[lucide--eraser]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("undo"), title: "Deshacer" }, h("span", { class: "icon-[lucide--undo-2]" })),
|
||||
h("button", { type: "button", class: "btn btn-ghost btn-xs", onclick: () => exec("redo"), title: "Rehacer" }, 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]" }))
|
||||
])
|
||||
h("button", { type: "button", class: () => `btn btn-ghost btn-xs ${isSource() ? 'btn-active' : ''}`, onclick: () => { if (!isSource()) source(editorRef?.innerHTML || ""); else if (editorRef) { editorRef.innerHTML = source(); notify(); }; isSource(!isSource()) } }, h("span", { class: "icon-[lucide--code-2]" }))
|
||||
])
|
||||
|
||||
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden", extraClass) }, [
|
||||
if (typeof document !== 'undefined' && !document.getElementById('editor-styles')) {
|
||||
const style = document.createElement('style')
|
||||
style.id = 'editor-styles'
|
||||
style.textContent = `
|
||||
[contenteditable="true"] div,
|
||||
[contenteditable="true"] p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
return h("div", { class: cls("border border-base-300 rounded-box bg-base-100 overflow-hidden shadow-sm flex flex-col", extraClass) }, [
|
||||
toolbar,
|
||||
h("div", { class: "relative" }, [
|
||||
h("div", { class: "relative flex-1 flex flex-col", onclick: () => showEmojis(false) }, [
|
||||
h("div", {
|
||||
ref: el => {
|
||||
if (!editorRef && el) {
|
||||
editorRef = el
|
||||
el.innerHTML = get(value) || ""
|
||||
editorRef = el; el.innerHTML = get(value) || "";
|
||||
document.execCommand("defaultParagraphSeparator", false, "br");
|
||||
el.addEventListener('click', (e) => {
|
||||
const container = e.target.closest('.resizable-img-container');
|
||||
if (container) { const img = container.querySelector('img'); if (img) openLightbox(img.src); }
|
||||
});
|
||||
}
|
||||
},
|
||||
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]",
|
||||
style: () => `min-height:22rem;${isSource() ? 'display:none' : ''}`,
|
||||
class: "p-4 outline-none text-base-content leading-relaxed [&>div]:m-0 [&>p]:m-0 [&>div]:min-h-[1em] [&_.resizable-img-container]:hover:border-primary [&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-8 [&_ol]:list-decimal [&_ol]:pl-8",
|
||||
contenteditable: "true",
|
||||
oninput: notify,
|
||||
onpaste: () => setTimeout(notify, 0)
|
||||
onkeydown: (e) => { if (e.key === 'Tab') { e.preventDefault(); exec("indent"); } },
|
||||
onkeyup: () => { triggerRefresh(); saveSelection(); },
|
||||
onclick: (e) => { triggerRefresh(); saveSelection(); e.stopPropagation(); },
|
||||
onmouseup: () => { notify(); saveSelection(); },
|
||||
onpaste: (e) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
exec('insertText', text);
|
||||
},
|
||||
ondragover: (e) => e.preventDefault(),
|
||||
ondrop: (e) => { e.preventDefault(); handleUpload(e.dataTransfer.files[0]) }
|
||||
}),
|
||||
h("textarea", {
|
||||
class: "w-full min-h-[10rem] p-3 outline-none font-mono text-sm bg-base-200 border-0",
|
||||
class: "w-full flex-1 min-h-[22rem] p-4 outline-none font-mono text-sm bg-base-200 border-0",
|
||||
style: () => isSource() ? '' : 'display:none',
|
||||
value: source,
|
||||
oninput: (e) => source(e.target.value)
|
||||
oninput: (e) => { source(e.target.value); if (editorRef) editorRef.innerHTML = e.target.value; p.onchange?.(e.target.value); }
|
||||
})
|
||||
]),
|
||||
h("div", { class: "px-3 py-1 border-t border-base-300 bg-base-100/50 text-[10px] text-right text-base-content/60 italic" }, [
|
||||
h("span", () => `${count()} caracteres`)
|
||||
])
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user