@navette-js/forms 1.0.0-beta.4 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +94 -7
- package/lib/index.js +256 -35
- package/lib/rich-content.d.ts +138 -0
- package/lib/rich-content.js +350 -0
- package/lib/rich-editor-tool.d.ts +60 -0
- package/lib/rich-editor-tool.js +89 -0
- package/lib/rich-editor-tools.d.ts +2 -0
- package/lib/rich-editor-tools.js +394 -0
- package/package.json +4 -4
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { HtmlString, type ColorManager, type Lazy } from '@navette-js/support';
|
|
2
|
-
import { Component, Schema, type ComponentContext } from '@navette-js/schemas';
|
|
2
|
+
import { Component, Schema, type ComponentContext, type ComponentSlot } from '@navette-js/schemas';
|
|
3
3
|
import { Action } from '@navette-js/actions';
|
|
4
|
+
import { TextColor, MentionProvider, type CustomBlockClass } from './rich-content.ts';
|
|
5
|
+
export { CustomBlockNode, GridColumnNode, GridNode, LeadNode, MentionNode, MentionProvider, MergeTagNode, registerRichContentExtensions, RichContentCustomBlock, RichContentRenderer, sanitizeCssColor, SizedImage, SmallMark, TextColor, TextColorMark, type CustomBlockClass } from './rich-content.ts';
|
|
6
|
+
import { RichEditorTool } from './rich-editor-tool.ts';
|
|
7
|
+
export { RichEditorTool, type RichEditorToolDefinition } from './rich-editor-tool.ts';
|
|
8
|
+
export { RICH_EDITOR_TOOL_DEFINITIONS } from './rich-editor-tools.ts';
|
|
4
9
|
export declare const useColorManager: (manager: ColorManager) => void;
|
|
5
10
|
import { type Rule } from './rules.ts';
|
|
6
11
|
export type { Rule } from './rules.ts';
|
|
@@ -39,6 +44,9 @@ export declare abstract class Field extends Component {
|
|
|
39
44
|
default(value: unknown): this;
|
|
40
45
|
/** Helper text shown under the field. */
|
|
41
46
|
helperText(text: string | null): this;
|
|
47
|
+
protected _belowContent: ComponentSlot;
|
|
48
|
+
/** Text, components or actions under the control (after the helper text). Strings are escaped. */
|
|
49
|
+
belowContent(content: ComponentSlot): this;
|
|
42
50
|
/** Helper text id, linked to the control through `aria-describedby`. */
|
|
43
51
|
protected getHelperTextId(): string | null;
|
|
44
52
|
/** Keeps the label for screen readers only. */
|
|
@@ -876,9 +884,18 @@ export declare function richHtmlToDocument(html: string | null | undefined): Ric
|
|
|
876
884
|
* TipTap document → HTML to save, or null if empty or invalid. The document comes from the browser,
|
|
877
885
|
* so it is rendered then re-parsed to keep only what the schema accepts.
|
|
878
886
|
*/
|
|
887
|
+
/** HTML of a document already sanitized (`RichContentRenderer`): no re-parsing. */
|
|
888
|
+
export declare function renderRichDocument(document: unknown): string;
|
|
879
889
|
export declare function richDocumentToHtml(document: unknown): string | null;
|
|
880
890
|
/** Tools of the rich editor's toolbar, in their default groups. */
|
|
881
891
|
export declare const RICH_EDITOR_TOOLS: string[][];
|
|
892
|
+
/** Custom blocks of an editor: a list, or groups of blocks by label. */
|
|
893
|
+
export type CustomBlocks = CustomBlockClass[] | Record<string, CustomBlockClass[]>;
|
|
894
|
+
/**
|
|
895
|
+
* Rich text editor (TipTap): the browser works on a document, the database receives HTML (or JSON with `json()`).
|
|
896
|
+
* Besides the starter kit: text colors, merge tags, mentions, custom blocks, columns, resizable images, and your own
|
|
897
|
+
* tools and TipTap extensions.
|
|
898
|
+
*/
|
|
882
899
|
export declare class RichEditor extends Field {
|
|
883
900
|
private _toolbar;
|
|
884
901
|
private _disabledTools;
|
|
@@ -888,12 +905,34 @@ export declare class RichEditor extends Field {
|
|
|
888
905
|
private _attachmentsDirectory;
|
|
889
906
|
private _attachmentTypes;
|
|
890
907
|
private _attachmentMaxSize;
|
|
908
|
+
private _customTools;
|
|
909
|
+
private _textColors;
|
|
910
|
+
private _customTextColors;
|
|
911
|
+
private _mergeTags;
|
|
912
|
+
private _noMergeTagResults;
|
|
913
|
+
private _mentions;
|
|
914
|
+
private _customBlocks;
|
|
915
|
+
private _minimalCustomBlockControls;
|
|
916
|
+
private _customBlocksGrid;
|
|
917
|
+
private _searchableCustomBlocks;
|
|
918
|
+
private _activePanel;
|
|
919
|
+
private _stickyToolbar;
|
|
920
|
+
private _resizableImages;
|
|
921
|
+
private _jsExtensions;
|
|
922
|
+
private _editorActions;
|
|
891
923
|
static make(name: string): RichEditor;
|
|
892
|
-
/** Toolbar tools, by group (`[['bold', 'italic', 'link'], ['h2', 'h3'], ['undo', 'redo']]`); see `RICH_EDITOR_TOOLS`. */
|
|
924
|
+
/** Toolbar tools, by group (`[['bold', 'italic', 'link'], ['h2', 'h3'], ['undo', 'redo']]`); see `RICH_EDITOR_TOOLS` and `RICH_EDITOR_TOOL_DEFINITIONS`. */
|
|
893
925
|
toolbarButtons(groups: string[][] | string[]): this;
|
|
926
|
+
private assertKnownTools;
|
|
894
927
|
/** Removes buttons; with no argument, removes them all. */
|
|
895
928
|
disableToolbarButtons(tools?: string[]): this;
|
|
896
929
|
disableAllToolbarButtons(condition?: boolean): this;
|
|
930
|
+
/** Your own toolbar buttons (or replacements of built-in ones, by name); place them with `toolbarButtons()`. */
|
|
931
|
+
tools(tools: RichEditorTool[]): this;
|
|
932
|
+
/** Actions mounted by your tools (`RichEditorTool.make('x').action()`), run with the editor selection in `arguments.editorSelection`. */
|
|
933
|
+
editorActions(actions: Action[]): this;
|
|
934
|
+
/** Browser TipTap extensions: URLs of ES modules exporting an extension (or an array of them). Register their server side with `registerRichContentExtensions()`. */
|
|
935
|
+
extensions(urls: string[]): this;
|
|
897
936
|
/** Saves the content as the editor's JSON document instead of HTML. */
|
|
898
937
|
json(condition?: boolean): this;
|
|
899
938
|
placeholder(text: string | null): this;
|
|
@@ -904,25 +943,73 @@ export declare class RichEditor extends Field {
|
|
|
904
943
|
fileAttachmentsAcceptedFileTypes(types: string[]): this;
|
|
905
944
|
/** Maximum size of attached files, in kilobytes. */
|
|
906
945
|
fileAttachmentsMaxSize(kilobytes: number): this;
|
|
946
|
+
/** Text colors offered by the `textColor` tool (the Tailwind palette by default): `{ brand: TextColor.make('Brand', '#0ea5e9', '#38bdf8') }`. */
|
|
947
|
+
textColors(colors: Record<string, TextColor> | null): this;
|
|
948
|
+
getTextColors(): Record<string, TextColor>;
|
|
949
|
+
/** Lets users pick any color, besides the listed ones. */
|
|
950
|
+
customTextColors(condition?: boolean): this;
|
|
951
|
+
/** Merge tags users insert (`mergeTags` tool and `{{` in the text), replaced by values when the content is rendered: `['name', 'today']` or `{ name: 'Name' }`. */
|
|
952
|
+
mergeTags(tags: string[] | Record<string, string>): this;
|
|
953
|
+
noMergeTagSearchResultsMessage(message: string | null): this;
|
|
954
|
+
/** Mentions: `[MentionProvider.make('@').items({ 1: 'Ada' })]`. */
|
|
955
|
+
mentions(providers: MentionProvider[]): this;
|
|
956
|
+
/** Custom blocks (`customBlocks` tool), as a list or grouped by label. */
|
|
957
|
+
customBlocks(blocks: CustomBlocks): this;
|
|
958
|
+
minimalCustomBlockControls(condition?: boolean): this;
|
|
959
|
+
/** Shows the blocks of the panel in a grid. */
|
|
960
|
+
customBlocksGrid(condition?: boolean): this;
|
|
961
|
+
/** Adds a search field to the blocks panel. */
|
|
962
|
+
searchableCustomBlocks(condition?: boolean): this;
|
|
963
|
+
/** Panel open when the editor loads (`customBlocks`, `mergeTags`). */
|
|
964
|
+
activePanel(panel: string | null): this;
|
|
965
|
+
/** Keeps the toolbar visible while scrolling the content. */
|
|
966
|
+
stickyToolbar(condition?: boolean): this;
|
|
967
|
+
/** Lets users resize images with handles; the size is saved on the image. */
|
|
968
|
+
resizableImages(condition?: boolean): this;
|
|
907
969
|
private attachmentsStorage;
|
|
908
|
-
private
|
|
970
|
+
private findTool;
|
|
971
|
+
private toolGroups;
|
|
972
|
+
private blockGroups;
|
|
973
|
+
getCustomBlock(id: unknown): CustomBlockClass | null;
|
|
909
974
|
hydrateState(value: unknown): unknown;
|
|
910
975
|
getDefaultState(): unknown;
|
|
911
976
|
/** HTML (sanitized by the editor's schema), or the JSON document with `json()` (normalized through the same schema). */
|
|
912
977
|
dehydrateState(state: unknown): unknown;
|
|
913
978
|
getValidationRules(): Promise<Rule[]>;
|
|
914
979
|
getExposedMethods(): string[];
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
980
|
+
private mentionProvider;
|
|
981
|
+
/** Mention search, for the editor's JavaScript: `[{ id, label }]`. */
|
|
982
|
+
getMentionSearchResultsForJs(search?: unknown, triggerCharacter?: unknown): Promise<{
|
|
983
|
+
id: string;
|
|
984
|
+
label: string;
|
|
985
|
+
}[]>;
|
|
986
|
+
/** Current labels of mentioned ids, by trigger character. */
|
|
987
|
+
getMentionLabelsForJs(mentions?: unknown): Promise<Record<string, Record<string, string>>>;
|
|
988
|
+
/** Previews of inserted blocks, for the editor's JavaScript. */
|
|
989
|
+
getCustomBlockPreviewsForJs(customBlocks?: unknown): {
|
|
990
|
+
key: number;
|
|
991
|
+
label: string;
|
|
992
|
+
preview: string;
|
|
993
|
+
shouldApplyProseStylingToPreview: boolean;
|
|
994
|
+
}[];
|
|
918
995
|
/**
|
|
919
996
|
* URL of a file the editor just uploaded: the file is checked (type from its content, size), stored, and its public URL returned.
|
|
920
997
|
* It stays stored even if the form is not saved.
|
|
921
998
|
*/
|
|
922
999
|
getUploadedFileAttachmentTemporaryUrl(args?: unknown): Promise<string | null>;
|
|
923
1000
|
saveUploadedFileAttachmentAndGetUrl(args?: unknown): Promise<string | null>;
|
|
924
|
-
/**
|
|
1001
|
+
/** Sends commands to the editor in the browser (after an action), on the given selection. */
|
|
1002
|
+
runCommands(livewire: any, commands: {
|
|
1003
|
+
name: string;
|
|
1004
|
+
arguments?: unknown[];
|
|
1005
|
+
}[], editorSelection?: unknown): void;
|
|
1006
|
+
/** Toolbar actions: link, text color, custom block, and the ones of `editorActions()`. */
|
|
925
1007
|
getActions(): Action[];
|
|
1008
|
+
private textColorAction;
|
|
1009
|
+
/** Modal of a custom block, built from the block class named in the arguments (`id`): insert or edit. */
|
|
1010
|
+
private customBlockAction;
|
|
1011
|
+
private customBlocksPanelHtml;
|
|
1012
|
+
private mergeTagsPanelHtml;
|
|
926
1013
|
toHtml(): Promise<string>;
|
|
927
1014
|
}
|
|
928
1015
|
/** Color picker (hex format). */
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Attributes, HtmlString, cls, e, evaluate, icon, js, sanitizeHtml, views, configured } from '@navette-js/support';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { t } from '@navette-js/support/i18n';
|
|
5
|
-
import { Component, Icon as SchemaIcon, Schema, Text, childSchema, renderInlineItems, slug } from '@navette-js/schemas';
|
|
5
|
+
import { Component, Icon as SchemaIcon, Schema, Text, childSchema, renderInlineItems, renderSlot, slug } from '@navette-js/schemas';
|
|
6
6
|
import { Action } from '@navette-js/actions';
|
|
7
7
|
import { loadingIndicator } from '@navette-js/support';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
@@ -12,7 +12,12 @@ import TextAlign from '@tiptap/extension-text-align';
|
|
|
12
12
|
import Subscript from '@tiptap/extension-subscript';
|
|
13
13
|
import Superscript from '@tiptap/extension-superscript';
|
|
14
14
|
import Highlight from '@tiptap/extension-highlight';
|
|
15
|
-
import
|
|
15
|
+
import { RICH_CONTENT_EXTENSIONS, SizedImage, customRichContentExtensions, registerTextColors, sanitizeCssColor, TextColor, textColorsForJs } from "./rich-content.js";
|
|
16
|
+
export { CustomBlockNode, GridColumnNode, GridNode, LeadNode, MentionNode, MentionProvider, MergeTagNode, registerRichContentExtensions, RichContentCustomBlock, RichContentRenderer, sanitizeCssColor, SizedImage, SmallMark, TextColor, TextColorMark } from "./rich-content.js";
|
|
17
|
+
import { RichEditorTool } from "./rich-editor-tool.js";
|
|
18
|
+
export { RichEditorTool } from "./rich-editor-tool.js";
|
|
19
|
+
import { RICH_EDITOR_TOOL_DEFINITIONS } from "./rich-editor-tools.js";
|
|
20
|
+
export { RICH_EDITOR_TOOL_DEFINITIONS } from "./rich-editor-tools.js";
|
|
16
21
|
import { TableKit } from '@tiptap/extension-table';
|
|
17
22
|
import { fileURLToPath } from 'node:url';
|
|
18
23
|
import nodePath from 'node:path';
|
|
@@ -61,6 +66,9 @@ export class Field extends Component {
|
|
|
61
66
|
default(value) { this._default = value; return this; }
|
|
62
67
|
/** Helper text shown under the field. */
|
|
63
68
|
helperText(text) { this._helperText = text; return this; }
|
|
69
|
+
_belowContent = null;
|
|
70
|
+
/** Text, components or actions under the control (after the helper text). Strings are escaped. */
|
|
71
|
+
belowContent(content) { this._belowContent = content; return this; }
|
|
64
72
|
/** Helper text id, linked to the control through `aria-describedby`. */
|
|
65
73
|
getHelperTextId() { return this._helperText !== null ? `${this.getId()}-helper-text` : null; }
|
|
66
74
|
/** Keeps the label for screen readers only. */
|
|
@@ -299,6 +307,8 @@ export class Field extends Component {
|
|
|
299
307
|
// Helper text: a small inline schema under the control.
|
|
300
308
|
if (this._helperText !== null)
|
|
301
309
|
html += `<div id="${e(this.getHelperTextId())}" class="fi-sc fi-inline fi-sc-has-gap fi-sc-dense"><div x-data="filamentSchemaComponent(${e(js({ key: null, exposedMethods: [], path: this.getStatePath(), containerPath: this.container.getStatePath() }))})"><div class="fi-sc-component"><span class="fi-sc-text">${e(this._helperText)}</span></div></div></div>`;
|
|
310
|
+
if (this._belowContent !== null)
|
|
311
|
+
html += await renderSlot(this, this._belowContent, this.getKey());
|
|
302
312
|
return views.render('filament-forms::components.field-wrapper', {
|
|
303
313
|
id: this.getId(), label: await this.getLabel(), required: (await this.isMarkedAsRequired()) && !(await this.isDisabled()),
|
|
304
314
|
labelPrefix: options.labelPrefix ?? '', hiddenLabel: this._hiddenLabel, after: options.after ?? '', afterLabel: this._hiddenLabel ? '' : await this.renderAfterLabel(), labelAsDiv: options.labelAsDiv === true, focusable: options.focusable === true, html, errorMessage: errors[0] ?? null, wrapperClass: options.wrapperClass ?? null,
|
|
@@ -1884,24 +1894,25 @@ export class ToggleButtons extends Field {
|
|
|
1884
1894
|
return this.wrap(`<div ${container.toHtml()}>${grid ? `<div${grid}>${items.join('')}</div>` : items.join('')}</div>`, { wrapperClass: 'fi-fo-toggle-buttons-wrp', labelAsDiv: true, focusable: true, after });
|
|
1885
1895
|
}
|
|
1886
1896
|
}
|
|
1887
|
-
// Server-side TipTap schema: the nodes and marks the rich editor
|
|
1888
|
-
|
|
1889
|
-
const RICH_EXTENSIONS = [StarterKit, TextAlign.configure({ types: ['heading', 'paragraph'], defaultAlignment: 'start' }), Subscript, Superscript, Highlight, Image, TableKit];
|
|
1897
|
+
// Server-side TipTap schema: the nodes and marks the rich editor can produce, and the extensions your application registers.
|
|
1898
|
+
const richExtensions = () => [StarterKit, TextAlign.configure({ types: ['heading', 'paragraph'], defaultAlignment: 'start' }), Subscript, Superscript, Highlight, SizedImage, TableKit, ...RICH_CONTENT_EXTENSIONS, ...customRichContentExtensions];
|
|
1890
1899
|
const isRichDocument = (value) => !!value && typeof value === 'object' && value.type === 'doc';
|
|
1891
1900
|
const richText = (node) => (typeof node?.text === 'string' ? node.text : '') + (Array.isArray(node?.content) ? node.content.map(richText).join('') : '');
|
|
1892
|
-
const hasRichContent = (node) =>
|
|
1901
|
+
const hasRichContent = (node) => ['image', 'horizontalRule', 'mergeTag', 'mention', 'customBlock'].includes(node?.type) || (Array.isArray(node?.content) && node.content.some(hasRichContent));
|
|
1893
1902
|
/** Stored HTML → TipTap document. Parsing drops anything the schema does not know (scripts, event attributes, `javascript:` links). */
|
|
1894
|
-
export function richHtmlToDocument(html) { return generateJSON(html ?? '',
|
|
1903
|
+
export function richHtmlToDocument(html) { return generateJSON(html ?? '', richExtensions()); }
|
|
1895
1904
|
/**
|
|
1896
1905
|
* TipTap document → HTML to save, or null if empty or invalid. The document comes from the browser,
|
|
1897
1906
|
* so it is rendered then re-parsed to keep only what the schema accepts.
|
|
1898
1907
|
*/
|
|
1908
|
+
/** HTML of a document already sanitized (`RichContentRenderer`): no re-parsing. */
|
|
1909
|
+
export function renderRichDocument(document) { return generateHTML(document, richExtensions()); }
|
|
1899
1910
|
export function richDocumentToHtml(document) {
|
|
1900
1911
|
if (!isRichDocument(document))
|
|
1901
1912
|
return null;
|
|
1902
1913
|
try {
|
|
1903
|
-
const clean = generateJSON(generateHTML(document,
|
|
1904
|
-
return richText(clean).trim() === '' && !hasRichContent(clean) ? null : generateHTML(clean,
|
|
1914
|
+
const clean = generateJSON(generateHTML(document, richExtensions()), richExtensions());
|
|
1915
|
+
return richText(clean).trim() === '' && !hasRichContent(clean) ? null : generateHTML(clean, richExtensions());
|
|
1905
1916
|
}
|
|
1906
1917
|
catch {
|
|
1907
1918
|
return null;
|
|
@@ -1936,6 +1947,15 @@ async function storeEditorAttachment(field, id, options) {
|
|
|
1936
1947
|
}
|
|
1937
1948
|
/** Tools of the rich editor's toolbar, in their default groups. */
|
|
1938
1949
|
export const RICH_EDITOR_TOOLS = [['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'link'], ['h2', 'h3'], ['alignStart', 'alignCenter', 'alignEnd'], ['blockquote', 'codeBlock', 'bulletList', 'orderedList'], ['table', 'attachFiles'], ['undo', 'redo']];
|
|
1950
|
+
/** The editor selection sent by the browser with a toolbar action. */
|
|
1951
|
+
const selectionOf = (args) => (args.editorSelection ?? null);
|
|
1952
|
+
// `details` needs a TipTap extension the server schema does not have: its content would be lost on save.
|
|
1953
|
+
const BUILT_IN_TOOLS = new Map(RICH_EDITOR_TOOL_DEFINITIONS.filter((definition) => definition.name !== 'details').map((definition) => [definition.name, definition]));
|
|
1954
|
+
/**
|
|
1955
|
+
* Rich text editor (TipTap): the browser works on a document, the database receives HTML (or JSON with `json()`).
|
|
1956
|
+
* Besides the starter kit: text colors, merge tags, mentions, custom blocks, columns, resizable images, and your own
|
|
1957
|
+
* tools and TipTap extensions.
|
|
1958
|
+
*/
|
|
1939
1959
|
export class RichEditor extends Field {
|
|
1940
1960
|
_toolbar = null;
|
|
1941
1961
|
_disabledTools = [];
|
|
@@ -1945,22 +1965,47 @@ export class RichEditor extends Field {
|
|
|
1945
1965
|
_attachmentsDirectory = null;
|
|
1946
1966
|
_attachmentTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
1947
1967
|
_attachmentMaxSize = 12288;
|
|
1968
|
+
_customTools = [];
|
|
1969
|
+
_textColors = null;
|
|
1970
|
+
_customTextColors = false;
|
|
1971
|
+
_mergeTags = {};
|
|
1972
|
+
_noMergeTagResults = null;
|
|
1973
|
+
_mentions = [];
|
|
1974
|
+
_customBlocks = [];
|
|
1975
|
+
_minimalCustomBlockControls = false;
|
|
1976
|
+
_customBlocksGrid = false;
|
|
1977
|
+
_searchableCustomBlocks = false;
|
|
1978
|
+
_activePanel = null;
|
|
1979
|
+
_stickyToolbar = false;
|
|
1980
|
+
_resizableImages = false;
|
|
1981
|
+
_jsExtensions = [];
|
|
1982
|
+
_editorActions = [];
|
|
1948
1983
|
static make(name) { return configured(new RichEditor(name)); }
|
|
1949
|
-
/** Toolbar tools, by group (`[['bold', 'italic', 'link'], ['h2', 'h3'], ['undo', 'redo']]`); see `RICH_EDITOR_TOOLS`. */
|
|
1984
|
+
/** Toolbar tools, by group (`[['bold', 'italic', 'link'], ['h2', 'h3'], ['undo', 'redo']]`); see `RICH_EDITOR_TOOLS` and `RICH_EDITOR_TOOL_DEFINITIONS`. */
|
|
1950
1985
|
toolbarButtons(groups) {
|
|
1951
1986
|
const list = (groups.length && typeof groups[0] === 'string' ? [groups] : groups);
|
|
1952
|
-
const known = new Set(RICH_EDITOR_TOOLS.flat());
|
|
1953
|
-
const unknown = list.flat().filter((tool) => !known.has(tool));
|
|
1954
|
-
if (unknown.length)
|
|
1955
|
-
throw new Error(`RichEditor: unknown toolbar buttons ${unknown.join(', ')} (available: ${[...known].join(', ')}).`);
|
|
1956
1987
|
this._toolbar = list;
|
|
1988
|
+
this.assertKnownTools();
|
|
1957
1989
|
return this;
|
|
1958
1990
|
}
|
|
1991
|
+
assertKnownTools() {
|
|
1992
|
+
if (!this._toolbar)
|
|
1993
|
+
return;
|
|
1994
|
+
const unknown = this._toolbar.flat().filter((tool) => !this.findTool(tool));
|
|
1995
|
+
if (unknown.length)
|
|
1996
|
+
throw new Error(`RichEditor: unknown toolbar buttons ${unknown.join(', ')} (available: ${[...BUILT_IN_TOOLS.keys(), ...this._customTools.map((tool) => tool.getName())].join(', ')}).`);
|
|
1997
|
+
}
|
|
1959
1998
|
/** Removes buttons; with no argument, removes them all. */
|
|
1960
1999
|
disableToolbarButtons(tools = []) { if (!tools.length)
|
|
1961
2000
|
this._toolbar = []; this._disabledTools = [...this._disabledTools, ...tools]; return this; }
|
|
1962
2001
|
disableAllToolbarButtons(condition = true) { if (condition)
|
|
1963
2002
|
this._toolbar = []; return this; }
|
|
2003
|
+
/** Your own toolbar buttons (or replacements of built-in ones, by name); place them with `toolbarButtons()`. */
|
|
2004
|
+
tools(tools) { this._customTools = [...this._customTools.filter((tool) => !tools.some((other) => other.getName() === tool.getName())), ...tools]; this.assertKnownTools(); return this; }
|
|
2005
|
+
/** Actions mounted by your tools (`RichEditorTool.make('x').action()`), run with the editor selection in `arguments.editorSelection`. */
|
|
2006
|
+
editorActions(actions) { this._editorActions = [...this._editorActions, ...actions]; return this; }
|
|
2007
|
+
/** Browser TipTap extensions: URLs of ES modules exporting an extension (or an array of them). Register their server side with `registerRichContentExtensions()`. */
|
|
2008
|
+
extensions(urls) { this._jsExtensions = [...this._jsExtensions, ...urls]; return this; }
|
|
1964
2009
|
/** Saves the content as the editor's JSON document instead of HTML. */
|
|
1965
2010
|
json(condition = true) { this._json = condition; return this; }
|
|
1966
2011
|
placeholder(text) { this._richPlaceholder = text; return this; }
|
|
@@ -1971,8 +2016,41 @@ export class RichEditor extends Field {
|
|
|
1971
2016
|
fileAttachmentsAcceptedFileTypes(types) { this._attachmentTypes = types.filter((type) => type.startsWith('image/') && type !== 'image/svg+xml'); return this; }
|
|
1972
2017
|
/** Maximum size of attached files, in kilobytes. */
|
|
1973
2018
|
fileAttachmentsMaxSize(kilobytes) { this._attachmentMaxSize = kilobytes; return this; }
|
|
2019
|
+
/** Text colors offered by the `textColor` tool (the Tailwind palette by default): `{ brand: TextColor.make('Brand', '#0ea5e9', '#38bdf8') }`. */
|
|
2020
|
+
textColors(colors) { this._textColors = colors; if (colors)
|
|
2021
|
+
registerTextColors(colors); return this; }
|
|
2022
|
+
getTextColors() { return this._textColors ?? TextColor.getDefaults(); }
|
|
2023
|
+
/** Lets users pick any color, besides the listed ones. */
|
|
2024
|
+
customTextColors(condition = true) { this._customTextColors = condition; return this; }
|
|
2025
|
+
/** Merge tags users insert (`mergeTags` tool and `{{` in the text), replaced by values when the content is rendered: `['name', 'today']` or `{ name: 'Name' }`. */
|
|
2026
|
+
mergeTags(tags) { this._mergeTags = Array.isArray(tags) ? Object.fromEntries(tags.map((tag) => [tag, tag])) : { ...tags }; return this; }
|
|
2027
|
+
noMergeTagSearchResultsMessage(message) { this._noMergeTagResults = message; return this; }
|
|
2028
|
+
/** Mentions: `[MentionProvider.make('@').items({ 1: 'Ada' })]`. */
|
|
2029
|
+
mentions(providers) { this._mentions = providers; return this; }
|
|
2030
|
+
/** Custom blocks (`customBlocks` tool), as a list or grouped by label. */
|
|
2031
|
+
customBlocks(blocks) { this._customBlocks = blocks; return this; }
|
|
2032
|
+
minimalCustomBlockControls(condition = true) { this._minimalCustomBlockControls = condition; return this; }
|
|
2033
|
+
/** Shows the blocks of the panel in a grid. */
|
|
2034
|
+
customBlocksGrid(condition = true) { this._customBlocksGrid = condition; return this; }
|
|
2035
|
+
/** Adds a search field to the blocks panel. */
|
|
2036
|
+
searchableCustomBlocks(condition = true) { this._searchableCustomBlocks = condition; return this; }
|
|
2037
|
+
/** Panel open when the editor loads (`customBlocks`, `mergeTags`). */
|
|
2038
|
+
activePanel(panel) { this._activePanel = panel; return this; }
|
|
2039
|
+
/** Keeps the toolbar visible while scrolling the content. */
|
|
2040
|
+
stickyToolbar(condition = true) { this._stickyToolbar = condition; return this; }
|
|
2041
|
+
/** Lets users resize images with handles; the size is saved on the image. */
|
|
2042
|
+
resizableImages(condition = true) { this._resizableImages = condition; return this; }
|
|
1974
2043
|
attachmentsStorage() { return this._attachmentsStorage ?? FileUpload.storage; }
|
|
1975
|
-
|
|
2044
|
+
findTool(name) {
|
|
2045
|
+
const custom = this._customTools.find((tool) => tool.getName() === name);
|
|
2046
|
+
if (custom)
|
|
2047
|
+
return custom;
|
|
2048
|
+
const definition = BUILT_IN_TOOLS.get(name);
|
|
2049
|
+
return definition ? RichEditorTool.fromDefinition(definition) : null;
|
|
2050
|
+
}
|
|
2051
|
+
toolGroups() { return (this._toolbar ?? RICH_EDITOR_TOOLS).map((group) => group.filter((tool) => !this._disabledTools.includes(tool) && (tool !== 'attachFiles' || this.attachmentsStorage() !== null))).filter((group) => group.length); }
|
|
2052
|
+
blockGroups() { return Array.isArray(this._customBlocks) ? [['', this._customBlocks]] : Object.entries(this._customBlocks); }
|
|
2053
|
+
getCustomBlock(id) { return this.blockGroups().flatMap(([, blocks]) => blocks).find((block) => block.getId() === id) ?? null; }
|
|
1976
2054
|
hydrateState(value) {
|
|
1977
2055
|
if (typeof value === 'string' && value.trim().startsWith('{')) {
|
|
1978
2056
|
try {
|
|
@@ -1997,9 +2075,43 @@ export class RichEditor extends Field {
|
|
|
1997
2075
|
return [...rules, ...this._rules];
|
|
1998
2076
|
}
|
|
1999
2077
|
getExposedMethods() { return ['getMentionSearchResultsForJs', 'getMentionLabelsForJs', 'getCustomBlockPreviewsForJs', 'getUploadedFileAttachmentTemporaryUrl', 'saveUploadedFileAttachmentAndGetUrl']; }
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2078
|
+
mentionProvider(char) { return this._mentions.find((provider) => provider.getTriggerCharacter() === char) ?? this._mentions[0] ?? null; }
|
|
2079
|
+
/** Mention search, for the editor's JavaScript: `[{ id, label }]`. */
|
|
2080
|
+
async getMentionSearchResultsForJs(search = '', triggerCharacter = '@') {
|
|
2081
|
+
const provider = this.mentionProvider(triggerCharacter ?? '@');
|
|
2082
|
+
if (!provider || (await this.isDisabled()))
|
|
2083
|
+
return [];
|
|
2084
|
+
return Object.entries(await provider.getSearchResults(typeof search === 'string' ? search.slice(0, 200) : '')).slice(0, 50).map(([id, label]) => ({ id, label }));
|
|
2085
|
+
}
|
|
2086
|
+
/** Current labels of mentioned ids, by trigger character. */
|
|
2087
|
+
async getMentionLabelsForJs(mentions = []) {
|
|
2088
|
+
const byChar = new Map();
|
|
2089
|
+
for (const mention of Array.isArray(mentions) ? mentions.slice(0, 500) : []) {
|
|
2090
|
+
const char = String(mention?.triggerCharacter ?? '@'), id = mention?.id;
|
|
2091
|
+
if (id === null || id === undefined)
|
|
2092
|
+
continue;
|
|
2093
|
+
byChar.set(char, [...(byChar.get(char) ?? []), String(id)]);
|
|
2094
|
+
}
|
|
2095
|
+
const labels = {};
|
|
2096
|
+
for (const [char, ids] of byChar) {
|
|
2097
|
+
const provider = this.mentionProvider(char);
|
|
2098
|
+
if (provider)
|
|
2099
|
+
labels[char] = await provider.getLabels(ids);
|
|
2100
|
+
}
|
|
2101
|
+
return labels;
|
|
2102
|
+
}
|
|
2103
|
+
/** Previews of inserted blocks, for the editor's JavaScript. */
|
|
2104
|
+
getCustomBlockPreviewsForJs(customBlocks = []) {
|
|
2105
|
+
const previews = [];
|
|
2106
|
+
for (const item of Array.isArray(customBlocks) ? customBlocks.slice(0, 200) : []) {
|
|
2107
|
+
const block = this.getCustomBlock(item?.id);
|
|
2108
|
+
const config = item?.config && typeof item.config === 'object' && !Array.isArray(item.config) ? item.config : {};
|
|
2109
|
+
if (!block || !Number.isInteger(item?.key))
|
|
2110
|
+
continue;
|
|
2111
|
+
previews.push({ key: item.key, label: block.getPreviewLabel(config), preview: Buffer.from(block.toPreviewHtml(config) ?? '').toString('base64'), shouldApplyProseStylingToPreview: block.shouldApplyProseStylingToPreview(config) });
|
|
2112
|
+
}
|
|
2113
|
+
return previews;
|
|
2114
|
+
}
|
|
2003
2115
|
/**
|
|
2004
2116
|
* URL of a file the editor just uploaded: the file is checked (type from its content, size), stored, and its public URL returned.
|
|
2005
2117
|
* It stays stored even if the form is not saved.
|
|
@@ -2007,15 +2119,19 @@ export class RichEditor extends Field {
|
|
|
2007
2119
|
async getUploadedFileAttachmentTemporaryUrl(args = {}) {
|
|
2008
2120
|
const id = String(args?.attachment ?? '');
|
|
2009
2121
|
const storage = this.attachmentsStorage();
|
|
2010
|
-
if (!storage || !/^[A-Za-z0-9_-]{1,64}$/.test(id) || !this.
|
|
2122
|
+
if (!storage || !/^[A-Za-z0-9_-]{1,64}$/.test(id) || !this.toolGroups().flat().includes('attachFiles') || (await this.isDisabled()))
|
|
2011
2123
|
return null;
|
|
2012
2124
|
return storeEditorAttachment(this, id, { storage, directory: this._attachmentsDirectory, types: this._attachmentTypes, maxSize: this._attachmentMaxSize });
|
|
2013
2125
|
}
|
|
2014
2126
|
async saveUploadedFileAttachmentAndGetUrl(args = {}) { return this.getUploadedFileAttachmentTemporaryUrl(args); }
|
|
2015
|
-
/**
|
|
2127
|
+
/** Sends commands to the editor in the browser (after an action), on the given selection. */
|
|
2128
|
+
runCommands(livewire, commands, editorSelection = null) {
|
|
2129
|
+
const key = this.getKey();
|
|
2130
|
+
livewire.dispatch('run-rich-editor-commands', { awaitSchemaComponent: key, livewireId: livewire.id, key, editorSelection, commands: commands.map((command) => ({ name: command.name, arguments: command.arguments ?? [] })) });
|
|
2131
|
+
}
|
|
2132
|
+
/** Toolbar actions: link, text color, custom block, and the ones of `editorActions()`. */
|
|
2016
2133
|
getActions() {
|
|
2017
2134
|
const tr = (name) => t(`filament-forms::components.rich_editor.actions.link.${name}`);
|
|
2018
|
-
const key = this.getKey();
|
|
2019
2135
|
return [
|
|
2020
2136
|
...this.getHintActions(),
|
|
2021
2137
|
Action.make('link').label(tr('label')).modalHeading(tr('modal.heading')).modalWidth('lg')
|
|
@@ -2027,20 +2143,107 @@ export class RichEditor extends Field {
|
|
|
2027
2143
|
Checkbox.make('shouldOpenInNewTab').label(tr('modal.form.should_open_in_new_tab.label')),
|
|
2028
2144
|
])
|
|
2029
2145
|
.action(({ livewire, arguments: args, data }) => {
|
|
2030
|
-
const selection = (args
|
|
2146
|
+
const selection = selectionOf(args);
|
|
2031
2147
|
const single = selection?.head === selection?.anchor;
|
|
2032
2148
|
const url = typeof data.url === 'string' && data.url !== '' ? data.url : null;
|
|
2033
|
-
|
|
2149
|
+
this.runCommands(livewire, [
|
|
2034
2150
|
...(single ? [{ name: 'extendMarkRange', arguments: ['link'] }] : []),
|
|
2035
|
-
url ? { name: 'setLink', arguments: [{ href: url, target: data.shouldOpenInNewTab ? '_blank' : null }] } : { name: 'unsetLink'
|
|
2036
|
-
];
|
|
2037
|
-
livewire.dispatch('run-rich-editor-commands', { awaitSchemaComponent: key, livewireId: livewire.id, key, editorSelection: selection, commands });
|
|
2151
|
+
url ? { name: 'setLink', arguments: [{ href: url, target: data.shouldOpenInNewTab ? '_blank' : null }] } : { name: 'unsetLink' },
|
|
2152
|
+
], selection);
|
|
2038
2153
|
}),
|
|
2154
|
+
this.textColorAction(),
|
|
2155
|
+
this.customBlockAction(),
|
|
2156
|
+
...this._editorActions,
|
|
2039
2157
|
];
|
|
2040
2158
|
}
|
|
2159
|
+
textColorAction() {
|
|
2160
|
+
const tr = (name) => t(`filament-forms::components.rich_editor.actions.text_color.${name}`);
|
|
2161
|
+
return Action.make('textColor').label(tr('label')).modalHeading(tr('modal.heading')).modalWidth('lg')
|
|
2162
|
+
.fillForm(({ arguments: args }) => {
|
|
2163
|
+
const color = typeof args.color === 'string' ? args.color : null;
|
|
2164
|
+
if (!color)
|
|
2165
|
+
return {};
|
|
2166
|
+
const known = Object.hasOwn(this.getTextColors(), color);
|
|
2167
|
+
return { color: known ? color : null, customColor: this._customTextColors && !known ? color : null };
|
|
2168
|
+
})
|
|
2169
|
+
.schema(() => {
|
|
2170
|
+
const options = Object.fromEntries(Object.entries(this.getTextColors()).map(([name, color]) => [name, `<div class="fi-fo-rich-editor-text-color-select-option"><div class="fi-fo-rich-editor-text-color-select-option-preview" style="--color: ${e(sanitizeCssColor(color.getColor()) ?? '')}; --dark-color: ${e(sanitizeCssColor(color.getDarkColor()) ?? '')}"></div><div>${e(color.getLabel() ?? name)}</div></div>`]));
|
|
2171
|
+
return [
|
|
2172
|
+
...(Object.keys(options).length ? [Select.make('color').label(tr('modal.form.color.label')).options(options).allowHtml().native(false).live()] : []),
|
|
2173
|
+
...(this._customTextColors ? [ColorPicker.make('customColor').label(tr('modal.form.custom_color.label')).visible(({ get }) => !get('color'))] : []),
|
|
2174
|
+
];
|
|
2175
|
+
})
|
|
2176
|
+
.action(({ livewire, arguments: args, data }) => {
|
|
2177
|
+
const selection = selectionOf(args);
|
|
2178
|
+
const single = selection?.head === selection?.anchor;
|
|
2179
|
+
const picked = typeof data.color === 'string' && Object.hasOwn(this.getTextColors(), data.color) ? data.color : this._customTextColors ? sanitizeCssColor(data.customColor) : null;
|
|
2180
|
+
this.runCommands(livewire, picked
|
|
2181
|
+
? [...(single ? [{ name: 'extendMarkRange', arguments: ['textColor'] }] : []), { name: 'setTextColor', arguments: [{ color: picked }] }]
|
|
2182
|
+
: [...(single ? [{ name: 'extendMarkRange', arguments: ['textColor'] }] : []), { name: 'unsetTextColor' }], selection);
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
/** Modal of a custom block, built from the block class named in the arguments (`id`): insert or edit. */
|
|
2186
|
+
customBlockAction() {
|
|
2187
|
+
const blockOf = (args) => this.getCustomBlock(args?.id);
|
|
2188
|
+
const configured = (args) => { const block = blockOf(args); return block ? block.configureEditorAction(Action.make('customBlock')) : null; };
|
|
2189
|
+
const argumentsOf = (livewire) => livewire?.mountedActions?.find?.((mounted) => mounted.name === 'customBlock')?.arguments ?? {};
|
|
2190
|
+
return Action.make('customBlock').modalWidth('lg')
|
|
2191
|
+
.modalHeading(({ arguments: args }) => blockOf(args)?.getLabel() ?? null)
|
|
2192
|
+
.modalSubmitActionLabel(({ arguments: args }) => t(`filament-forms::components.rich_editor.actions.custom_block.modal.actions.${args.mode === 'edit' ? 'save' : 'insert'}.label`))
|
|
2193
|
+
.modalHidden(async (ctx) => { const action = configured(ctx.arguments); return !action || (await action.isModalHidden(ctx)); })
|
|
2194
|
+
.fillForm(({ arguments: args }) => (args.config && typeof args.config === 'object' && !Array.isArray(args.config) ? args.config : {}))
|
|
2195
|
+
.schema((schema) => configured(argumentsOf(schema.livewire))?.applySchema(schema) ?? schema.components([]))
|
|
2196
|
+
.action(({ livewire, arguments: args, data }) => {
|
|
2197
|
+
const block = blockOf(args);
|
|
2198
|
+
if (!block)
|
|
2199
|
+
return;
|
|
2200
|
+
const config = data ?? {};
|
|
2201
|
+
const content = { type: 'customBlock', attrs: { config, id: block.getId(), label: block.getPreviewLabel(config), preview: Buffer.from(block.toPreviewHtml(config) ?? '').toString('base64'), shouldApplyProseStylingToPreview: block.shouldApplyProseStylingToPreview(config) } };
|
|
2202
|
+
const selection = { ...(selectionOf(args) ?? {}) };
|
|
2203
|
+
if (args.dragPosition !== undefined && args.dragPosition !== null) {
|
|
2204
|
+
this.runCommands(livewire, [{ name: 'insertContentAt', arguments: [args.dragPosition, content] }]);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (selection.type === 'node' && args.mode === 'insert') {
|
|
2208
|
+
this.runCommands(livewire, [{ name: 'insertContentAt', arguments: [Number(selection.anchor ?? -1) + 1, content] }]);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (args.mode === 'edit' && selection.type !== 'node') {
|
|
2212
|
+
selection.type = 'node';
|
|
2213
|
+
selection.anchor = Number(selection.anchor ?? 1) - 1;
|
|
2214
|
+
delete selection.head;
|
|
2215
|
+
}
|
|
2216
|
+
this.runCommands(livewire, [{ name: 'insertContent', arguments: [content] }], selection);
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
customBlocksPanelHtml(key) {
|
|
2220
|
+
const searchable = this._searchableCustomBlocks;
|
|
2221
|
+
const groups = this.blockGroups();
|
|
2222
|
+
const allLabels = groups.flatMap(([label, blocks]) => [label, ...blocks.map((block) => block.getLabel())]).filter(Boolean);
|
|
2223
|
+
const parts = [];
|
|
2224
|
+
if (searchable)
|
|
2225
|
+
parts.push(`<div class="fi-fo-rich-editor-custom-blocks-search">${icon('m-magnifying-glass')}<input type="search" x-model="customBlockSearch" x-on:keydown.enter.prevent aria-label="${e(t('filament-forms::components.rich_editor.custom_blocks.search_label'))}" placeholder="${e(t('filament-forms::components.rich_editor.custom_blocks.search_prompt'))}" class="fi-fo-rich-editor-custom-blocks-search-input" /></div>`);
|
|
2226
|
+
const list = groups.map(([groupLabel, blocks]) => {
|
|
2227
|
+
const groupLabels = [groupLabel, ...blocks.map((block) => block.getLabel())];
|
|
2228
|
+
const show = (labels) => (searchable ? ` x-show="${e(`matchesCustomBlockSearch(${js(labels)})`)}"` : '');
|
|
2229
|
+
const header = groupLabel ? `<h4${show(groupLabels)} class="fi-fo-rich-editor-custom-blocks-group-header">${e(groupLabel)}</h4>` : '';
|
|
2230
|
+
const buttons = blocks.map((block) => {
|
|
2231
|
+
const id = block.getId();
|
|
2232
|
+
const blockIcon = block.getIcon();
|
|
2233
|
+
return `<button draggable="true" type="button" data-block-id="${e(id)}" x-data="{ isLoading: false }"${show([groupLabel, block.getLabel()])} x-on:click="${e(`isLoading = true\n$wire.mountAction('customBlock', { editorSelection, id: ${js(id)}, mode: 'insert' }, { schemaComponent: ${js(key)} })`)}" x-on:dragstart="${e(`$event.dataTransfer.setData('customBlock', ${js(id)})`)}" x-on:open-modal.window="isLoading = false" x-on:run-rich-editor-commands.window="isLoading = false" class="fi-fo-rich-editor-custom-block-btn">${blockIcon ? `<span x-show="! isLoading" class="fi-fo-rich-editor-custom-block-icon">${icon(blockIcon)}</span>` : ''}${loadingIndicator(new Attributes({ 'x-show': 'isLoading', 'x-cloak': '' }))}<span>${e(block.getLabel())}</span></button>`;
|
|
2234
|
+
}).join('');
|
|
2235
|
+
return `${header}<div${show(groupLabels)} class="${cls(['fi-fo-rich-editor-custom-blocks-list', { 'fi-fo-rich-editor-custom-blocks-grid': this._customBlocksGrid }])}">${buttons}</div>`;
|
|
2236
|
+
}).join('');
|
|
2237
|
+
const noResults = searchable ? `<p x-show="${e(`! matchesCustomBlockSearch(${js(allLabels)})`)}" x-cloak role="status" class="fi-fo-rich-editor-custom-blocks-no-results">${e(t('filament-forms::components.rich_editor.custom_blocks.no_search_results_message'))}</p>` : '';
|
|
2238
|
+
return `${parts.join('')}<div class="fi-fo-rich-editor-custom-blocks-ctn">${list}${noResults}</div>`;
|
|
2239
|
+
}
|
|
2240
|
+
mergeTagsPanelHtml() {
|
|
2241
|
+
return Object.entries(this._mergeTags).map(([id, label]) => `<button draggable="true" type="button" x-on:click="${e(`insertMergeTag(${js(id)})`)}" x-on:dragstart="${e(`$event.dataTransfer.setData('mergeTag', ${js(id)})`)}" class="fi-fo-rich-editor-merge-tag-btn"><span data-type="mergeTag" data-id="${e(id)}">${e(label)}</span></button>`).join('');
|
|
2242
|
+
}
|
|
2041
2243
|
async toHtml() {
|
|
2042
|
-
|
|
2043
|
-
|
|
2244
|
+
const key = this.getKey();
|
|
2245
|
+
let html = views.render('filament-forms::components.rich-editor', { livewireId: this.livewire.id, key, statePath: this.getStatePath(), label: await this.getLabel(), js });
|
|
2246
|
+
// Toolbar: the configured groups, each tool rendered from its definition.
|
|
2044
2247
|
const toolbarStart = html.indexOf('data-rich-editor-toolbar="main"');
|
|
2045
2248
|
if (toolbarStart >= 0) {
|
|
2046
2249
|
const open = html.indexOf('>', toolbarStart) + 1;
|
|
@@ -2050,24 +2253,42 @@ export class RichEditor extends Field {
|
|
|
2050
2253
|
if (close < 0)
|
|
2051
2254
|
throw new Error('RichEditor: unexpected view markup (toolbar end not found).');
|
|
2052
2255
|
const body = html.slice(open, close);
|
|
2053
|
-
const
|
|
2054
|
-
for (const match of body.matchAll(/<button data-rich-editor-tool="([A-Za-z0-9]+)"[\s\S]*?<\/button>/g))
|
|
2055
|
-
buttons.set(match[1], match[0]);
|
|
2056
|
-
const groups = this.tools().map((group) => `<div class="fi-fo-rich-editor-toolbar-group">${group.map((tool) => buttons.get(tool) ?? '').join('')}</div>`).join('');
|
|
2256
|
+
const groups = this.toolGroups().map((group) => `<div class="fi-fo-rich-editor-toolbar-group">${group.map((tool) => this.findTool(tool)?.toHtml() ?? '').join('')}</div>`).join('');
|
|
2057
2257
|
const endOfToolbar = body.lastIndexOf('</div>');
|
|
2058
2258
|
html = html.slice(0, open) + groups + body.slice(endOfToolbar) + html.slice(close);
|
|
2259
|
+
if (this._stickyToolbar)
|
|
2260
|
+
html = html.replace('class="fi-fo-rich-editor-toolbar "', 'class="fi-fo-rich-editor-toolbar fi-fo-rich-editor-sticky-toolbar"');
|
|
2261
|
+
}
|
|
2262
|
+
// Panels: custom blocks and merge tags.
|
|
2263
|
+
const blocksStart = html.indexOf('<div class="fi-fo-rich-editor-custom-blocks-ctn">');
|
|
2264
|
+
if (blocksStart >= 0) {
|
|
2265
|
+
const blocksEnd = html.indexOf('</div>', blocksStart) + '</div>'.length;
|
|
2266
|
+
html = html.slice(0, blocksStart) + this.customBlocksPanelHtml(key) + html.slice(blocksEnd);
|
|
2267
|
+
}
|
|
2268
|
+
const tagsStart = html.indexOf('<div class="fi-fo-rich-editor-merge-tags-list">');
|
|
2269
|
+
if (tagsStart >= 0) {
|
|
2270
|
+
const open = html.indexOf('>', tagsStart) + 1;
|
|
2271
|
+
html = html.slice(0, open) + this.mergeTagsPanelHtml() + html.slice(html.indexOf('</div>', open));
|
|
2059
2272
|
}
|
|
2060
|
-
const attach = this.
|
|
2061
|
-
const
|
|
2273
|
+
const attach = this.toolGroups().flat().includes('attachFiles');
|
|
2274
|
+
const mentions = await Promise.all(this._mentions.map((provider) => provider.toJs()));
|
|
2062
2275
|
html = html.replace('canAttachFiles: true,', `canAttachFiles: ${attach ? 'true' : 'false'},`)
|
|
2063
2276
|
.replace('maxFileSize: 12288,', `maxFileSize: ${this._attachmentMaxSize},`)
|
|
2064
2277
|
.replace(/maxFileSizeValidationMessage: '[^']*',/, () => `maxFileSizeValidationMessage: ${js(`Uploaded files must not be greater than ${this._attachmentMaxSize} kilobytes.`)},`)
|
|
2065
2278
|
.replace(/acceptedFileTypes: JSON\.parse\('[^']*'\),/, () => `acceptedFileTypes: ${js(this._attachmentTypes)},`)
|
|
2066
2279
|
.replace(/acceptedFileTypesValidationMessage: '[^']*',/, () => `acceptedFileTypesValidationMessage: ${js(`Uploaded files must be of type: ${this._attachmentTypes.join(', ')}.`)},`)
|
|
2280
|
+
.replace('activePanel: null,', () => `activePanel: ${js(this._activePanel)},`)
|
|
2281
|
+
.replace('extensions: [],', () => `extensions: ${js(this._jsExtensions)},`)
|
|
2282
|
+
.replace('hasResizableImages: false,', `hasResizableImages: ${this._resizableImages ? 'true' : 'false'},`)
|
|
2283
|
+
.replace('hasMinimalCustomBlockControls: false,', `hasMinimalCustomBlockControls: ${this._minimalCustomBlockControls ? 'true' : 'false'},`)
|
|
2284
|
+
.replace('hasStickyToolbar: false,', `hasStickyToolbar: ${this._stickyToolbar ? 'true' : 'false'},`)
|
|
2285
|
+
.replace('mentions: [],', () => `mentions: ${js(mentions)},`)
|
|
2286
|
+
.replace('mergeTags: [],', () => `mergeTags: ${js(Object.keys(this._mergeTags).length ? this._mergeTags : [])},`)
|
|
2287
|
+
.replace(/noMergeTagSearchResultsMessage: '[^']*',/, () => `noMergeTagSearchResultsMessage: ${js(this._noMergeTagResults ?? t('filament-forms::components.rich_editor.no_merge_tag_search_results_message'))},`)
|
|
2288
|
+
.replace(/textColors: JSON\.parse\('[^']*'\),/, () => `textColors: ${js(textColorsForJs(this.getTextColors()))},`)
|
|
2067
2289
|
.replace('placeholder: null,', `placeholder: ${js(this._richPlaceholder)},`)
|
|
2068
2290
|
.replace('isLiveOnBlur: false,', `isLiveOnBlur: ${this._liveMode === 'blur' ? 'true' : 'false'},`)
|
|
2069
2291
|
.replace(/state: \$wire\.\$entangle\(('[^']*'), false\)/, (_m, path) => `state: $wire.$entangle(${path}, ${this._liveMode === 'live' ? 'true' : 'false'})`);
|
|
2070
|
-
void tr;
|
|
2071
2292
|
return this.wrap(html, { labelAsDiv: true });
|
|
2072
2293
|
}
|
|
2073
2294
|
}
|