use a custom tinymce svelte component and other minor optimizations
This commit is contained in:
+1
-2
@@ -14,7 +14,7 @@
|
||||
import { admin } from "@/stores/admin";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { resetConfirmation } from "@/stores/confirmation";
|
||||
import TinyMCE from "@tinymce/tinymce-svelte";
|
||||
import TinyMCE from "@/components/base/TinyMCE.svelte";
|
||||
|
||||
let oldLocation = undefined;
|
||||
|
||||
@@ -149,7 +149,6 @@
|
||||
{#if showAppSidebar && !isTinyMCEPreloaded}
|
||||
<div class="tinymce-preloader hidden">
|
||||
<TinyMCE
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
conf={CommonHelper.defaultEditorOptions()}
|
||||
on:init={() => {
|
||||
isTinyMCEPreloaded = true;
|
||||
|
||||
@@ -175,8 +175,6 @@
|
||||
clearTimeout(contentScrollThrottle);
|
||||
contentScrollThrottle = null;
|
||||
|
||||
console.log("here");
|
||||
|
||||
if (!panel) {
|
||||
return; // deleted during timeout
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<script context="module">
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* The below component is similar to https://github.com/tinymce/tinymce-svelte
|
||||
* but with removed unnecessary dependencies (eg. the TinyMCE cloud loading script)
|
||||
* and with extra error catching to handle the async edge-cases
|
||||
* when the init event is fired after the Svelte component was destroyed.
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function createScriptLoader() {
|
||||
let state = {
|
||||
listeners: [],
|
||||
scriptLoaded: false,
|
||||
injected: false,
|
||||
};
|
||||
|
||||
function injectScript(doc, url, callback) {
|
||||
state.injected = true;
|
||||
const script = doc.createElement("script");
|
||||
script.referrerPolicy = "origin";
|
||||
script.type = "application/javascript";
|
||||
script.src = url;
|
||||
script.onload = () => {
|
||||
callback();
|
||||
};
|
||||
if (doc.head) {
|
||||
doc.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
function load(doc, url, callback) {
|
||||
if (state.scriptLoaded) {
|
||||
callback();
|
||||
} else {
|
||||
state.listeners.push(callback);
|
||||
// check we can access doc
|
||||
if (!state.injected) {
|
||||
injectScript(doc, url, () => {
|
||||
state.listeners.forEach((fn) => fn());
|
||||
state.scriptLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { load };
|
||||
}
|
||||
|
||||
let scriptLoader = createScriptLoader();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
|
||||
export let id = "tinymce_svelte" + CommonHelper.randomString(7);
|
||||
export let inline = undefined;
|
||||
export let disabled = false;
|
||||
export let scriptSrc = `${import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js`;
|
||||
export let conf = {};
|
||||
export let modelEvents = "change input undo redo";
|
||||
export let value = "";
|
||||
export let text = "";
|
||||
export let cssClass = "tinymce-wrapper";
|
||||
|
||||
// Events
|
||||
// ---------------------------------------------------------------
|
||||
const validEvents = [
|
||||
"Activate",
|
||||
"AddUndo",
|
||||
"BeforeAddUndo",
|
||||
"BeforeExecCommand",
|
||||
"BeforeGetContent",
|
||||
"BeforeRenderUI",
|
||||
"BeforeSetContent",
|
||||
"BeforePaste",
|
||||
"Blur",
|
||||
"Change",
|
||||
"ClearUndos",
|
||||
"Click",
|
||||
"ContextMenu",
|
||||
"Copy",
|
||||
"Cut",
|
||||
"Dblclick",
|
||||
"Deactivate",
|
||||
"Dirty",
|
||||
"Drag",
|
||||
"DragDrop",
|
||||
"DragEnd",
|
||||
"DragGesture",
|
||||
"DragOver",
|
||||
"Drop",
|
||||
"ExecCommand",
|
||||
"Focus",
|
||||
"FocusIn",
|
||||
"FocusOut",
|
||||
"GetContent",
|
||||
"Hide",
|
||||
"Init",
|
||||
"KeyDown",
|
||||
"KeyPress",
|
||||
"KeyUp",
|
||||
"LoadContent",
|
||||
"MouseDown",
|
||||
"MouseEnter",
|
||||
"MouseLeave",
|
||||
"MouseMove",
|
||||
"MouseOut",
|
||||
"MouseOver",
|
||||
"MouseUp",
|
||||
"NodeChange",
|
||||
"ObjectResizeStart",
|
||||
"ObjectResized",
|
||||
"ObjectSelected",
|
||||
"Paste",
|
||||
"PostProcess",
|
||||
"PostRender",
|
||||
"PreProcess",
|
||||
"ProgressState",
|
||||
"Redo",
|
||||
"Remove",
|
||||
"Reset",
|
||||
"ResizeEditor",
|
||||
"SaveContent",
|
||||
"SelectionChange",
|
||||
"SetAttrib",
|
||||
"SetContent",
|
||||
"Show",
|
||||
"Submit",
|
||||
"Undo",
|
||||
"VisualAid",
|
||||
];
|
||||
|
||||
const bindHandlers = (editor, dispatch) => {
|
||||
validEvents.forEach((eventName) => {
|
||||
editor.on(eventName, (e) => {
|
||||
dispatch(eventName.toLowerCase(), {
|
||||
eventName,
|
||||
event: e,
|
||||
editor,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
let container;
|
||||
let element;
|
||||
let editorRef;
|
||||
|
||||
let lastVal = value;
|
||||
let disablindCache = disabled;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: {
|
||||
try {
|
||||
if (editorRef && lastVal !== value) {
|
||||
editorRef.setContent(value);
|
||||
text = editorRef.getContent({ format: "text" });
|
||||
}
|
||||
if (editorRef && disabled !== disablindCache) {
|
||||
disablindCache = disabled;
|
||||
if (typeof editorRef.mode?.set === "function") {
|
||||
editorRef.mode.set(disabled ? "readonly" : "design");
|
||||
} else {
|
||||
editorRef.setMode(disabled ? "readonly" : "design");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("TinyMCE reactive error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function getTinymce() {
|
||||
return window && window.tinymce ? window.tinymce : null;
|
||||
}
|
||||
|
||||
function init() {
|
||||
const finalInit = {
|
||||
...conf,
|
||||
target: element,
|
||||
inline: inline !== undefined ? inline : conf.inline !== undefined ? conf.inline : false,
|
||||
readonly: disabled,
|
||||
setup: (editor) => {
|
||||
editorRef = editor;
|
||||
editor.on("init", () => {
|
||||
editor.setContent(value);
|
||||
// bind model events
|
||||
editor.on(modelEvents, () => {
|
||||
lastVal = editor.getContent();
|
||||
if (lastVal !== value) {
|
||||
value = lastVal;
|
||||
text = editor.getContent({ format: "text" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bindHandlers(editor, dispatch);
|
||||
|
||||
if (typeof conf.setup === "function") {
|
||||
conf.setup(editor);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
element.style.visibility = "";
|
||||
|
||||
getTinymce().init(finalInit);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (getTinymce() !== null) {
|
||||
init();
|
||||
} else {
|
||||
scriptLoader.load(container.ownerDocument, scriptSrc, () => {
|
||||
// init if the container is not removed from the DOM
|
||||
if (container) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
if (editorRef) {
|
||||
getTinymce()?.remove(editorRef);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class={cssClass}>
|
||||
{#if inline}
|
||||
<div {id} bind:this={element} />
|
||||
{:else}
|
||||
<textarea {id} bind:this={element} style="visibility: hidden" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
import CopyIcon from "@/components/base/CopyIcon.svelte";
|
||||
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
||||
import RecordInfo from "@/components/records/RecordInfo.svelte";
|
||||
import TinyMCE from "@tinymce/tinymce-svelte";
|
||||
import TinyMCE from "@/components/base/TinyMCE.svelte";
|
||||
|
||||
export let record;
|
||||
export let field;
|
||||
@@ -52,19 +52,17 @@
|
||||
</span>
|
||||
{:else}
|
||||
<TinyMCE
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
cssClass="tinymce-preview"
|
||||
conf={{
|
||||
branding: false,
|
||||
promotion: false,
|
||||
menubar: false,
|
||||
min_height: 30,
|
||||
statusbar: false,
|
||||
min_height: 30,
|
||||
height: 59,
|
||||
max_height: 500,
|
||||
autoresize_bottom_margin: 5,
|
||||
resize: false,
|
||||
skin: "pocketbase",
|
||||
content_style: "body { font-size: 14px }",
|
||||
toolbar: "",
|
||||
plugins: ["autoresize"],
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
export let collection;
|
||||
|
||||
let recordPanel;
|
||||
let original = null;
|
||||
let record = null;
|
||||
let original = {};
|
||||
let record = {};
|
||||
let initialDraft = null;
|
||||
let isSaving = false;
|
||||
let confirmHide = false; // prevent close recursion
|
||||
@@ -472,47 +472,55 @@
|
||||
{isNew ? "New" : "Edit"}
|
||||
<strong>{collection?.name}</strong> record
|
||||
</h4>
|
||||
{/if}
|
||||
|
||||
{#if !isNew}
|
||||
<div class="flex-fill" />
|
||||
<button type="button" aria-label="More" class="btn btn-sm btn-circle btn-transparent flex-gap-0">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
||||
{#if isAuthCollection && !original.verified && original.email}
|
||||
{#if !isNew}
|
||||
<div class="flex-fill" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More"
|
||||
class="btn btn-sm btn-circle btn-transparent flex-gap-0"
|
||||
>
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
||||
{#if isAuthCollection && !original.verified && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendVerificationEmail()}
|
||||
>
|
||||
<i class="ri-mail-check-line" />
|
||||
<span class="txt">Send verification email</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if isAuthCollection && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendPasswordResetEmail()}
|
||||
>
|
||||
<i class="ri-mail-lock-line" />
|
||||
<span class="txt">Send password reset email</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendVerificationEmail()}
|
||||
on:click={() => duplicateConfirm()}
|
||||
>
|
||||
<i class="ri-mail-check-line" />
|
||||
<span class="txt">Send verification email</span>
|
||||
<i class="ri-file-copy-line" />
|
||||
<span class="txt">Duplicate</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if isAuthCollection && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendPasswordResetEmail()}
|
||||
class="dropdown-item txt-danger closable"
|
||||
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-mail-lock-line" />
|
||||
<span class="txt">Send password reset email</span>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="dropdown-item closable" on:click={() => duplicateConfirm()}>
|
||||
<i class="ri-file-copy-line" />
|
||||
<span class="txt">Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger closable"
|
||||
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isAuthCollection && !isNew}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import TinyMCE from "@tinymce/tinymce-svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import TinyMCE from "@/components/base/TinyMCE.svelte";
|
||||
import RecordFilePicker from "@/components/records/RecordFilePicker.svelte";
|
||||
|
||||
export let field;
|
||||
@@ -25,7 +25,11 @@
|
||||
value = "";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (typeof value == "undefined") {
|
||||
value = "";
|
||||
}
|
||||
|
||||
// slight "offset" the editor mount to avoid blocking the rendering of the other fields
|
||||
mountedTimeoutId = setTimeout(() => {
|
||||
mounted = true;
|
||||
@@ -45,7 +49,6 @@
|
||||
{#if mounted}
|
||||
<TinyMCE
|
||||
id={uniqueId}
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
{conf}
|
||||
bind:value
|
||||
on:init={(initEvent) => {
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class CommonHelper {
|
||||
tagName === "input" ||
|
||||
tagName === "select" ||
|
||||
tagName === "textarea" ||
|
||||
element.isContentEditable
|
||||
element?.isContentEditable
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export default class CommonHelper {
|
||||
tagName === "button" ||
|
||||
tagName === "a" ||
|
||||
tagName === "details" ||
|
||||
element.tabIndex >= 0
|
||||
element?.tabIndex >= 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user