[#370] added rich text editor field
This commit is contained in:
@@ -31,6 +31,10 @@
|
||||
}, 200);
|
||||
}
|
||||
|
||||
export function isExpanded() {
|
||||
return !!active;
|
||||
}
|
||||
|
||||
export function expand() {
|
||||
collapseSiblings();
|
||||
active = true;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import BoolOptions from "@/components/collections/schema/BoolOptions.svelte";
|
||||
import EmailOptions from "@/components/collections/schema/EmailOptions.svelte";
|
||||
import UrlOptions from "@/components/collections/schema/UrlOptions.svelte";
|
||||
import EditorOptions from "@/components/collections/schema/EditorOptions.svelte";
|
||||
import DateOptions from "@/components/collections/schema/DateOptions.svelte";
|
||||
import SelectOptions from "@/components/collections/schema/SelectOptions.svelte";
|
||||
import JsonOptions from "@/components/collections/schema/JsonOptions.svelte";
|
||||
@@ -262,6 +263,8 @@
|
||||
<EmailOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "editor"}
|
||||
<EditorOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "date"}
|
||||
<DateOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "select"}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
// svelte-ignore unused-export-let
|
||||
export let key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export let options = {};
|
||||
</script>
|
||||
@@ -9,10 +9,15 @@
|
||||
|
||||
const types = [
|
||||
{
|
||||
label: "Text",
|
||||
label: "Plain text",
|
||||
value: "text",
|
||||
icon: CommonHelper.getFieldTypeIcon("text"),
|
||||
},
|
||||
{
|
||||
label: "Rich editor",
|
||||
value: "editor",
|
||||
icon: CommonHelper.getFieldTypeIcon("editor"),
|
||||
},
|
||||
{
|
||||
label: "Number",
|
||||
value: "number",
|
||||
@@ -43,11 +48,6 @@
|
||||
value: "select",
|
||||
icon: CommonHelper.getFieldTypeIcon("select"),
|
||||
},
|
||||
{
|
||||
label: "JSON",
|
||||
value: "json",
|
||||
icon: CommonHelper.getFieldTypeIcon("json"),
|
||||
},
|
||||
{
|
||||
label: "File",
|
||||
value: "file",
|
||||
@@ -58,12 +58,12 @@
|
||||
value: "relation",
|
||||
icon: CommonHelper.getFieldTypeIcon("relation"),
|
||||
},
|
||||
{
|
||||
label: "JSON",
|
||||
value: "json",
|
||||
icon: CommonHelper.getFieldTypeIcon("json"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<ObjectSelect
|
||||
class="field-type-select {classes}"
|
||||
items={types}
|
||||
bind:keyOfSelected={value}
|
||||
{...$$restProps}
|
||||
/>
|
||||
<ObjectSelect class="field-type-select {classes}" items={types} bind:keyOfSelected={value} {...$$restProps} />
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// rough text cut to avoid rendering large chunk of texts
|
||||
function cutText(text) {
|
||||
text = text || "";
|
||||
return text.length > 200 ? text.substring(0, 200) : text;
|
||||
return text.length > 150 ? text.substring(0, 150) : text;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,8 +31,12 @@
|
||||
use:tooltip={"Open in new tab"}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{record[field.name]}
|
||||
{cutText(record[field.name])}
|
||||
</a>
|
||||
{:else if field.type === "editor"}
|
||||
<span class="txt txt-ellipsis">
|
||||
{cutText(CommonHelper.plainText(record[field.name]))}
|
||||
</span>
|
||||
{:else if field.type === "date"}
|
||||
<FormattedDate date={record[field.name]} />
|
||||
{:else if field.type === "json"}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import JsonField from "@/components/records/fields/JsonField.svelte";
|
||||
import FileField from "@/components/records/fields/FileField.svelte";
|
||||
import RelationField from "@/components/records/fields/RelationField.svelte";
|
||||
import EditorField from "@/components/records/fields/EditorField.svelte";
|
||||
import ExternalAuthsList from "@/components/records/ExternalAuthsList.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -350,6 +351,8 @@
|
||||
<EmailField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "editor"}
|
||||
<EditorField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "date"}
|
||||
<DateField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "select"}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import TinyMCE from "@tinymce/tinymce-svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<TinyMCE
|
||||
id={uniqueId}
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
conf={CommonHelper.defaultEditorOptions()}
|
||||
bind:value
|
||||
/>
|
||||
</Field>
|
||||
@@ -82,12 +82,20 @@
|
||||
<input type="text" id={uniqueId} bind:value={config.subject} spellcheck="false" required />
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</span>,
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</span>.
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -96,17 +104,28 @@
|
||||
<input type="text" id={uniqueId} bind:value={config.actionUrl} spellcheck="false" required />
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</span>,
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</span>,
|
||||
<span
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
title="Required parameter"
|
||||
on:click={() => copy("{TOKEN}")}>{"{TOKEN}"}</span
|
||||
>.
|
||||
on:click={() => copy("{TOKEN}")}
|
||||
>
|
||||
{"{TOKEN}"}
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -128,22 +147,35 @@
|
||||
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</span>,
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</span>,
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{TOKEN}")}>
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{TOKEN}")}
|
||||
>
|
||||
{"{TOKEN}"}
|
||||
</span>,
|
||||
<span
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
title="Required parameter"
|
||||
on:click={() => copy("{ACTION_URL}")}
|
||||
>
|
||||
{"{ACTION_URL}"}
|
||||
</span>.
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
</Accordion>
|
||||
|
||||
+43
-9
@@ -999,7 +999,7 @@ select {
|
||||
|
||||
.field-type-select {
|
||||
.options-dropdown {
|
||||
padding: 2px;
|
||||
padding: 2px 1px 1px 2px;
|
||||
.form-field.options-search {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1009,22 +1009,15 @@ select {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.dropdown-item {
|
||||
width: 50%;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding-left: 12px; // visual align with the label
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--baseAlt2Color);
|
||||
&:nth-child(2n) {
|
||||
border-left: 1px solid var(--baseAlt2Color);
|
||||
}
|
||||
&:nth-last-child(-n+2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
border-right: 1px solid var(--baseAlt2Color);
|
||||
&.selected {
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
@@ -1147,3 +1140,44 @@ select {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tinymce field
|
||||
.tinymce-wrapper {
|
||||
@extend %input;
|
||||
min-height: 277px;
|
||||
.tox-tinymce {
|
||||
border-radius: var(--baseRadius);
|
||||
border: 0;
|
||||
}
|
||||
.form-field label ~ & {
|
||||
padding: 5px 2px 2px 2px;
|
||||
}
|
||||
}
|
||||
body {
|
||||
.tox .tox-tbtn {
|
||||
height: 30px;
|
||||
svg {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
}
|
||||
.tox .tox-tbtn:not(.tox-tbtn--select) {
|
||||
width: 30px;
|
||||
}
|
||||
.tox .tox-button,
|
||||
.tox .tox-button--secondary {
|
||||
font-size: var(--smFontSize);
|
||||
}
|
||||
.tox .tox-toolbar-overlord {
|
||||
@include shadowize();
|
||||
}
|
||||
.tox .tox-listboxfield .tox-listbox--select,
|
||||
.tox .tox-textarea, .tox .tox-textfield,
|
||||
.tox .tox-toolbar-textfield {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.tox-swatch:not(.tox-swatch--remove):not(.tox-collection__item--enabled) {
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@
|
||||
overflow-y: auto; /* fallback */
|
||||
overflow-y: overlay;
|
||||
scroll-behavior: smooth;
|
||||
.tox-fullscreen & {
|
||||
z-index: 9;
|
||||
}
|
||||
}
|
||||
.panel-header ~ .panel-content {
|
||||
padding-top: 5px;
|
||||
|
||||
+104
-18
@@ -457,6 +457,22 @@ export default class CommonHelper {
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plain text version (aka. strip tags) of the provided string.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
*/
|
||||
static plainText(str) {
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const doc = new DOMParser().parseFromString(str, "text/html");
|
||||
|
||||
return (doc.body.innerText || "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes and converts the provided string to a slug.
|
||||
*
|
||||
@@ -804,24 +820,6 @@ export default class CommonHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default Flatpickr initialization options.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static defaultFlatpickrOptions() {
|
||||
return {
|
||||
dateFormat: "Y-m-d H:i:S",
|
||||
disableMobile: true,
|
||||
allowInput: true,
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
locale: {
|
||||
firstDayOfWeek: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a dummy collection record object.
|
||||
*
|
||||
@@ -972,6 +970,8 @@ export default class CommonHelper {
|
||||
return "ri-mail-line";
|
||||
case "url":
|
||||
return "ri-link";
|
||||
case "editor":
|
||||
return "ri-edit-2-line";
|
||||
case "select":
|
||||
return "ri-list-check";
|
||||
case "json":
|
||||
@@ -1136,4 +1136,90 @@ export default class CommonHelper {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default Flatpickr initialization options.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static defaultFlatpickrOptions() {
|
||||
return {
|
||||
dateFormat: "Y-m-d H:i:S",
|
||||
disableMobile: true,
|
||||
allowInput: true,
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
locale: {
|
||||
firstDayOfWeek: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default rich editor options.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static defaultEditorOptions() {
|
||||
return {
|
||||
branding: false,
|
||||
promotion: false,
|
||||
menubar: false,
|
||||
min_height: 270,
|
||||
height: 270,
|
||||
max_height: 700,
|
||||
autoresize_bottom_margin: 30,
|
||||
skin: "pocketbase",
|
||||
content_css: "pocketbase",
|
||||
content_style: "body { font-size: 14px }",
|
||||
plugins: [
|
||||
"autoresize",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"searchreplace",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"code",
|
||||
],
|
||||
toolbar:
|
||||
"code undo redo insert | styles | bold italic | alignleft aligncenter alignright | bullist numlist | link image table | forecolor backcolor fullscreen",
|
||||
file_picker_types: "image",
|
||||
// @see https://www.tiny.cloud/docs/tinymce/6/file-image-upload/#interactive-example
|
||||
file_picker_callback: (cb, value, meta) => {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
input.setAttribute("accept", "image/*");
|
||||
|
||||
input.addEventListener("change", (e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener("load", () => {
|
||||
if (!tinymce) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to register the blob in TinyMCEs image blob registry.
|
||||
// In future TinyMCE version this part will be handled internally.
|
||||
const id = "blobid" + new Date().getTime();
|
||||
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
|
||||
const base64 = reader.result.split(",")[1];
|
||||
const blobInfo = blobCache.create(id, file, base64);
|
||||
blobCache.add(blobInfo);
|
||||
|
||||
// call the callback and populate the Title field with the file name
|
||||
cb(blobInfo.blobUri(), { title: file.name });
|
||||
});
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
input.click();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user