rich editor files

This commit is contained in:
2024-08-18 17:23:18 +03:00
parent ec15f21e67
commit 5d6869c118
20 changed files with 966 additions and 64 deletions
-10
View File
@@ -9,15 +9,6 @@ Mustache.escape = function (value) {
return value;
};
function enableTooltipsAnywhere() {
// Enable tooltips everywhere
let tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// Define all components
const entryComponents = {
@@ -61,4 +52,3 @@ let loadSvelte = function () {
// document.addEventListener("turbo:load", loadSvelte);
document.addEventListener("DOMContentLoaded", loadSvelte);
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
+1 -1
View File
@@ -96,7 +96,7 @@
<span>{record._file.width + "x" + record._file.height}</span>
{/if}
<a
href="{fileurl(record)}"
href="{fileurl(channel,record)}"
target="_blank"
>
Download
+1 -1
View File
@@ -40,7 +40,7 @@
>
<img
class="rounded w-100"
src={imgurl(record)}
src={imgurl(channel,record)}
alt={record._file.path}
/>
</a>
+25 -7
View File
@@ -1,15 +1,33 @@
import {getContext} from "svelte";
export function imgurl(record) {
export function imgurl(channel,record) {
if(record._file.mime === "image/svg+xml"){
return fileurl(record);
if (record._file.mime === "image/svg+xml") {
return fileurl(channel, record);
}
const channel = getContext("channel")
return channel.filesUrl + `/thumbs/${record._file.path}`;
}
export function fileurl(record) {
const channel = getContext("channel")
export function fileurl(channel, record) {
return channel.filesUrl + `/${record._file.path}`;
}
export function htmlurl(channel,record, preset) {
let html = "";
let url = fileurl(channel,record)
if (record._file.width > 0) {
let presetUrl = url;
if (preset) {
presetUrl = channel.filesUrl + `/templates/${preset}/${record._file.path}`;
}
html = `<img src="${presetUrl}" alt="${record._file.path}" />`
} else if (record._file.mime === "image/svg+xml") {
html = `<img src="${url}" alt="${record._file.path}"/>`
} else {
html = `<a href="${url}">${record._file.originalName}</a>`
}
return html;
}
+3
View File
@@ -100,6 +100,9 @@
tinymce.init({...config, ...additionalConfig});
});
export function insertMedia(html){
activeEditor.execCommand('InsertHTML', false, html);
}
</script>
+96
View File
@@ -0,0 +1,96 @@
<script>
import {onDestroy, onMount} from 'svelte';
import {Editor} from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Heading from '@tiptap/extension-heading'
import Blockquote from '@tiptap/extension-blockquote';
import Bold from '@tiptap/extension-bold';
import BulletList from '@tiptap/extension-bullet-list';
import Code from '@tiptap/extension-code';
import History from '@tiptap/extension-history';
import Italic from '@tiptap/extension-italic';
import ListItem from '@tiptap/extension-list-item';
import OrderedList from '@tiptap/extension-ordered-list';
import Strike from '@tiptap/extension-strike';
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header';
import Underline from '@tiptap/extension-underline';
let element;
let editor;
export let value = "";
onMount(() => {
editor = new Editor({
element: element,
extensions: [
Document,
Paragraph,
Text,
Bold,
BulletList,
Code,
History,
Italic,
ListItem,
OrderedList,
ListItem,
Strike,
Table,
TableRow,
TableCell,
TableHeader,
Underline,
Heading.configure({
levels: [1, 2, 3],
}),
Blockquote
],
content: value,
editable: true,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
},
});
});
onDestroy(() => {
if (editor) {
editor.destroy();
}
});
</script>
{#if editor}
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
class:active={editor.isActive('heading', { level: 1 })}
>
H1
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
class:active={editor.isActive('heading', { level: 2 })}
>
H2
</button>
<button
on:click={() => editor.chain().focus().setParagraph().run()}
class:active={editor.isActive('paragraph')}
>
P
</button>
<button
on:click={() => editor.chain().focus().toggleBold().run()}
class:active={editor.isActive('bold')}
>
Bold
</button>
{/if}
<div bind:this={element} class="content"/>
+21
View File
@@ -0,0 +1,21 @@
<script>
import {onMount} from "svelte";
import Trix from "trix"
import customcss from "./tinymce.css?inline";
import "trix/dist/trix.css"
export let value = "";
let textareaEl;
let lastVal;
let editorWrapper;
let activeEditor;
Trix.config.blockAttributes.default.breakOnReturn = false
console.log(Trix.config)
</script>
<div bind:this={editorWrapper} class="tox-wrapper">
<input bind:this={textareaEl} id="x" bind:value type="hidden">
<trix-editor class="trix-content content" input="x"></trix-editor>
</div>
+1 -1
View File
@@ -40,7 +40,7 @@
<span>{record._file.checksum}</span>
</div>
<div class="file-details-item">
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(record)}">Download</a>
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(channel,record)}">Download</a>
</div>
</div>
</div>
+10
View File
@@ -96,6 +96,16 @@
{isCreateMode}
{id}
/>
{:else if field.info.name === "rich"}
<RichEditor
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else}
<svelte:component
this={formElement}
+2
View File
@@ -37,5 +37,7 @@
{graph}
/>
</div>
{:else}
Nothing links to this record
{/each}
</div>
@@ -1,20 +1,44 @@
<script>
import Tinymce from "../../libs/Tinymce.svelte";
import RichEditorFiles from "./RichEditorFiles.svelte";
import {getErrorMessage} from "./errorMessage";
export let value;
export let field;
export let isCreateMode;
export let schema;
export let graph;
export let record;
export let validationErrors;
let editor;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let additionalConfig = {
readonly: field.readonly && !isCreateMode,
};
function insertMedia(e){
editor.insertMedia(e.detail)
}
</script>
<div class="mb-0">
<Tinymce bind:value {additionalConfig} {schema}/>
<Tinymce bind:this={editor} bind:value {additionalConfig}/>
{#if field.collections}
<RichEditorFiles
bind:graph
{record}
{field}
{validationErrors}
on:editor-insert={insertMedia}
>
</RichEditorFiles>
{/if}
<!-- <TipTap bind:value />-->
{#if errorMessage}
<div class="invalid-feedback d-block">
@@ -0,0 +1,77 @@
<script>
import {sortByField} from "../../edges/sortEdges";
import Sortable from "../../libs/Sortable.svelte";
import PreviewFile from "../previews/PreviewFile.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import Dialog from "../../dialog/Dialog.svelte";
import {insertEdges} from "./reference.js";
import {getContext} from "svelte";
const channel = getContext("channel");
export let field;
export let record;
export let graph
let browseModal;
$: references = graph?.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
function insert(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
}
</script>
<div class="mb-3">
<label class="mt-4 mb-3">Rich editor files</label>
{#if field.collections.length === 1}
<button
class="button"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<Dropdown>
<div slot="button">
Browse
</div>
{#each collections as collection}
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) => openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
{/each}
</Dropdown>
{/if}
</div>
{#if references.length > 0}
{#each references as reference (reference.id)}
<!--This div helps the sorting thing-->
<div>
<PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference} on:editor-insert></PreviewFile>
</div>
{/each}
{/if}
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
@@ -4,21 +4,31 @@
import {createEventDispatcher, getContext} from "svelte";
import Preview from "../../files/Preview.svelte";
import {previewTitle} from "./../Preview";
import {htmlurl} from "../../files/imageserver.js"
import Status from "./../Status.svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let record;
export let hasDelete = false;
export let hasInsert = false;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
let cardTitle = previewTitle(channel.schemas, record);
let imagePresets = Object.keys(channel.imageFilters);
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
function insert(e, preset) {
e.preventDefault();
let html = htmlurl(channel,record, preset)
dispatch("editor-insert", html);
}
</script>
<div class="preview-file">
@@ -45,15 +55,31 @@
</div>
</div>
{#if hasDelete}
<div class="trash-action">
<button
class="button"
on:click={remove}
>
<Icon icon="trash-can"/>
</button>
</div>
{/if}
<div style="display: flex;gap:4px; align-items: center; margin-right: 10px;">
{#if hasInsert}
<div class="reference-action">
<Dropdown>
<div slot="button">
<Icon icon="photo-film"/>
</div>
<button class="dropdown-item button" on:click={e => insert(e,null)}>original</button>
{#each imagePresets as preset}
<button class="dropdown-item button" on:click={e => insert(e,preset)}>{preset}</button>
{/each}
</Dropdown>
</div>
{/if}
{#if hasDelete}
<div class="reference-action">
<button
class="button"
on:click={remove}
>
<Icon icon="trash-can"/>
</button>
</div>
{/if}
</div>
</div>