use a custom tinymce svelte component and other minor optimizations

This commit is contained in:
Gani Georgiev
2023-10-15 14:04:44 +03:00
parent c0fa53a2ab
commit 8868fa9ae6
106 changed files with 541 additions and 326 deletions
@@ -175,8 +175,6 @@
clearTimeout(contentScrollThrottle);
contentScrollThrottle = null;
console.log("here");
if (!panel) {
return; // deleted during timeout
}
+241
View File
@@ -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) => {