diff --git a/resources/icons/editor/image-search.svg b/resources/icons/editor/image-search.svg
new file mode 100644
index 000000000..b8cb2cfc8
--- /dev/null
+++ b/resources/icons/editor/image-search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/js/wysiwyg/nodes/diagram.ts b/resources/js/wysiwyg/nodes/diagram.ts
index e2ffeaadd..bd37b200c 100644
--- a/resources/js/wysiwyg/nodes/diagram.ts
+++ b/resources/js/wysiwyg/nodes/diagram.ts
@@ -3,15 +3,12 @@ import {
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
- LexicalEditor, LexicalNode,
+ LexicalEditor,
SerializedLexicalNode,
Spread
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
-import * as DrawIO from '../../services/drawio';
-import {EditorUiContext} from "../ui/framework/core";
-import {HttpError} from "../../services/http";
import {el} from "../utils/dom";
export type SerializedDiagramNode = Spread<{
@@ -156,69 +153,3 @@ export class DiagramNode extends DecoratorNode {
export function $createDiagramNode(drawingId: string = '', drawingUrl: string = ''): DiagramNode {
return new DiagramNode(drawingId, drawingUrl);
}
-
-export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
- return node instanceof DiagramNode;
-}
-
-
-function handleUploadError(error: HttpError, context: EditorUiContext): void {
- if (error.status === 413) {
- window.$events.emit('error', context.options.translations.serverUploadLimitText || '');
- } else {
- window.$events.emit('error', context.options.translations.imageUploadErrorText || '');
- }
- console.error(error);
-}
-
-async function loadDiagramIdFromNode(editor: LexicalEditor, node: DiagramNode): Promise {
- const drawingId = await new Promise((res, rej) => {
- editor.getEditorState().read(() => {
- const {id: drawingId} = node.getDrawingIdAndUrl();
- res(drawingId);
- });
- });
-
- return drawingId || '';
-}
-
-async function updateDrawingNodeFromData(context: EditorUiContext, node: DiagramNode, pngData: string, isNew: boolean): Promise {
- DrawIO.close();
-
- if (isNew) {
- const loadingImage: string = window.baseUrl('/loading.gif');
- context.editor.update(() => {
- node.setDrawingIdAndUrl('', loadingImage);
- });
- }
-
- try {
- const img = await DrawIO.upload(pngData, context.options.pageId);
- context.editor.update(() => {
- node.setDrawingIdAndUrl(String(img.id), img.url);
- });
- } catch (err) {
- if (err instanceof HttpError) {
- handleUploadError(err, context);
- }
-
- if (isNew) {
- context.editor.update(() => {
- node.remove();
- });
- }
-
- throw new Error(`Failed to save image with error: ${err}`);
- }
-}
-
-export function $openDrawingEditorForNode(context: EditorUiContext, node: DiagramNode): void {
- let isNew = false;
- DrawIO.show(context.options.drawioUrl, async () => {
- const drawingId = await loadDiagramIdFromNode(context.editor, node);
- isNew = !drawingId;
- return isNew ? '' : DrawIO.load(drawingId);
- }, async (pngData: string) => {
- return updateDrawingNodeFromData(context, node, pngData, isNew);
- });
-}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index c8a0293d5..1b10ef91b 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,7 +2,7 @@
## In progress
-//
+//
## Main Todo
@@ -12,8 +12,6 @@
- Keyboard shortcuts support
- Link popup menu for cross-content reference
- Link heading-based ID reference menu
-- Image gallery integration for insert
-- Image gallery integration for form
- Drawing gallery integration
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
- Media resize support (like images)
diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/diagram.ts
index 7c79f9f41..44d332939 100644
--- a/resources/js/wysiwyg/ui/decorators/diagram.ts
+++ b/resources/js/wysiwyg/ui/decorators/diagram.ts
@@ -1,8 +1,9 @@
import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core";
import {BaseSelection} from "lexical";
-import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
+import {DiagramNode} from "../../nodes/diagram";
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
+import {$openDrawingEditorForNode} from "../../utils/diagrams";
export class DiagramDecorator extends EditorDecorator {
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
index 0eac497fc..f4075a740 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
@@ -5,7 +5,7 @@ import {
$createNodeSelection,
$createTextNode,
$getRoot,
- $getSelection,
+ $getSelection, $insertNodes,
$setSelection,
BaseSelection,
ElementNode
@@ -20,7 +20,7 @@ import codeBlockIcon from "@icons/editor/code-block.svg";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../../nodes/code-block";
import editIcon from "@icons/edit.svg";
import diagramIcon from "@icons/editor/diagram.svg";
-import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../../nodes/diagram";
+import {$createDiagramNode, DiagramNode} from "../../../nodes/diagram";
import detailsIcon from "@icons/editor/details.svg";
import mediaIcon from "@icons/editor/media.svg";
import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
@@ -30,6 +30,9 @@ import {
$insertNewBlockNodeAtSelection,
$selectionContainsNodeType
} from "../../../utils/selection";
+import {$isDiagramNode, $openDrawingEditorForNode} from "../../../utils/diagrams";
+import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
+import {$showImageForm} from "../forms/objects";
export const link: EditorButtonDefinition = {
label: 'Insert/edit link',
@@ -94,28 +97,19 @@ export const image: EditorButtonDefinition = {
label: 'Insert/Edit Image',
icon: imageIcon,
action(context: EditorUiContext) {
- const imageModal = context.manager.createModal('image');
- const selection = context.lastSelection;
- const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
-
context.editor.getEditorState().read(() => {
- let formDefaults = {};
+ const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode) as ImageNode | null;
if (selectedImage) {
- formDefaults = {
- src: selectedImage.getSrc(),
- alt: selectedImage.getAltText(),
- height: selectedImage.getHeight(),
- width: selectedImage.getWidth(),
- }
-
- context.editor.update(() => {
- const selection = $createNodeSelection();
- selection.add(selectedImage.getKey());
- $setSelection(selection);
- });
+ $showImageForm(selectedImage, context);
+ return;
}
- imageModal.show(formDefaults);
+ showImageManager((image) => {
+ context.editor.update(() => {
+ const link = $createLinkedImageNodeFromImageData(image);
+ $insertNodes([link]);
+ });
+ })
});
},
isActive(selection: BaseSelection | null): boolean {
diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
index dbb89b18f..c37696695 100644
--- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
@@ -1,31 +1,78 @@
-import {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../../framework/forms";
+import {
+ EditorFormDefinition,
+ EditorFormField,
+ EditorFormTabs,
+ EditorSelectFormFieldDefinition
+} from "../../framework/forms";
import {EditorUiContext} from "../../framework/core";
import {$createTextNode, $getSelection} from "lexical";
-import {$createImageNode} from "../../../nodes/image";
+import {$isImageNode, ImageNode} from "../../../nodes/image";
import {$createLinkNode} from "@lexical/link";
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
import {$insertNodeToNearestRoot} from "@lexical/utils";
import {$getNodeFromSelection} from "../../../utils/selection";
+import {EditorFormModal} from "../../framework/modals";
+import {EditorActionField} from "../../framework/blocks/action-field";
+import {EditorButton} from "../../framework/buttons";
+import {showImageManager} from "../../../utils/images";
+import searchImageIcon from "@icons/editor/image-search.svg";
+
+export function $showImageForm(image: ImageNode, context: EditorUiContext) {
+ const imageModal: EditorFormModal = context.manager.createModal('image');
+ const height = image.getHeight();
+ const width = image.getWidth();
+
+ const formData = {
+ src: image.getSrc(),
+ alt: image.getAltText(),
+ height: height === 0 ? '' : String(height),
+ width: width === 0 ? '' : String(width),
+ };
+
+ imageModal.show(formData);
+}
export const image: EditorFormDefinition = {
submitText: 'Apply',
async action(formData, context: EditorUiContext) {
context.editor.update(() => {
- const selection = $getSelection();
- const imageNode = $createImageNode(formData.get('src')?.toString() || '', {
- alt: formData.get('alt')?.toString() || '',
- height: Number(formData.get('height')?.toString() || '0'),
- width: Number(formData.get('width')?.toString() || '0'),
- });
- selection?.insertNodes([imageNode]);
+ const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode);
+ if ($isImageNode(selectedImage)) {
+ selectedImage.setSrc(formData.get('src')?.toString() || '');
+ selectedImage.setAltText(formData.get('alt')?.toString() || '');
+
+ selectedImage.setWidth(Number(formData.get('width')?.toString() || '0'));
+ selectedImage.setHeight(Number(formData.get('height')?.toString() || '0'));
+ }
});
return true;
},
fields: [
{
- label: 'Source',
- name: 'src',
- type: 'text',
+ build() {
+ return new EditorActionField(
+ new EditorFormField({
+ label: 'Source',
+ name: 'src',
+ type: 'text',
+ }),
+ new EditorButton({
+ label: 'Browse files',
+ icon: searchImageIcon,
+ action(context: EditorUiContext) {
+ showImageManager((image) => {
+ const modal = context.manager.getActiveModal('image');
+ if (modal) {
+ modal.getForm().setValues({
+ src: image.thumbs?.display || image.url,
+ alt: image.name,
+ });
+ }
+ });
+ }
+ }),
+ );
+ },
},
{
label: 'Alternative description',
diff --git a/resources/js/wysiwyg/ui/framework/blocks/action-field.ts b/resources/js/wysiwyg/ui/framework/blocks/action-field.ts
new file mode 100644
index 000000000..1f40c2864
--- /dev/null
+++ b/resources/js/wysiwyg/ui/framework/blocks/action-field.ts
@@ -0,0 +1,26 @@
+import {EditorContainerUiElement, EditorUiElement} from "../core";
+import {el} from "../../../utils/dom";
+import {EditorFormField} from "../forms";
+import {EditorButton} from "../buttons";
+
+
+export class EditorActionField extends EditorContainerUiElement {
+ protected input: EditorFormField;
+ protected action: EditorButton;
+
+ constructor(input: EditorFormField, action: EditorButton) {
+ super([input, action]);
+
+ this.input = input;
+ this.action = action;
+ }
+
+ buildDOM(): HTMLElement {
+ return el('div', {
+ class: 'editor-action-input-container',
+ }, [
+ this.input.getDOMElement(),
+ this.action.getDOMElement(),
+ ]);
+ }
+}
diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts
index 29d959910..92891b540 100644
--- a/resources/js/wysiwyg/ui/framework/manager.ts
+++ b/resources/js/wysiwyg/ui/framework/manager.ts
@@ -11,6 +11,7 @@ export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
export class EditorUIManager {
protected modalDefinitionsByKey: Record = {};
+ protected activeModalsByKey: Record = {};
protected decoratorConstructorsByType: Record = {};
protected decoratorInstancesByNodeKey: Record = {};
protected context: EditorUiContext|null = null;
@@ -50,12 +51,24 @@ export class EditorUIManager {
throw new Error(`Attempted to show modal of key [${key}] but no modal registered for that key`);
}
- const modal = new EditorFormModal(modalDefinition);
+ const modal = new EditorFormModal(modalDefinition, key);
modal.setContext(this.getContext());
return modal;
}
+ setModalActive(key: string, modal: EditorFormModal): void {
+ this.activeModalsByKey[key] = modal;
+ }
+
+ setModalInactive(key: string): void {
+ delete this.activeModalsByKey[key];
+ }
+
+ getActiveModal(key: string): EditorFormModal|null {
+ return this.activeModalsByKey[key];
+ }
+
registerDecoratorType(type: string, decorator: typeof EditorDecorator) {
this.decoratorConstructorsByType[type] = decorator;
}
diff --git a/resources/js/wysiwyg/ui/framework/modals.ts b/resources/js/wysiwyg/ui/framework/modals.ts
index 1768f6f54..ae69302f6 100644
--- a/resources/js/wysiwyg/ui/framework/modals.ts
+++ b/resources/js/wysiwyg/ui/framework/modals.ts
@@ -13,10 +13,12 @@ export interface EditorFormModalDefinition extends EditorModalDefinition {
export class EditorFormModal extends EditorContainerUiElement {
protected definition: EditorFormModalDefinition;
+ protected key: string;
- constructor(definition: EditorFormModalDefinition) {
+ constructor(definition: EditorFormModalDefinition, key: string) {
super([new EditorForm(definition.form)]);
this.definition = definition;
+ this.key = key;
}
show(defaultValues: Record) {
@@ -26,13 +28,16 @@ export class EditorFormModal extends EditorContainerUiElement {
const form = this.getForm();
form.setValues(defaultValues);
form.setOnCancel(this.hide.bind(this));
+
+ this.getContext().manager.setModalActive(this.key, this);
}
hide() {
this.getDOMElement().remove();
+ this.getContext().manager.setModalInactive(this.key);
}
- protected getForm(): EditorForm {
+ getForm(): EditorForm {
return this.children[0] as EditorForm;
}
diff --git a/resources/js/wysiwyg/utils/diagrams.ts b/resources/js/wysiwyg/utils/diagrams.ts
new file mode 100644
index 000000000..50d7d5b3f
--- /dev/null
+++ b/resources/js/wysiwyg/utils/diagrams.ts
@@ -0,0 +1,70 @@
+import {LexicalEditor, LexicalNode} from "lexical";
+import {HttpError} from "../../services/http";
+import {EditorUiContext} from "../ui/framework/core";
+import * as DrawIO from "../../services/drawio";
+import {DiagramNode} from "../nodes/diagram";
+
+export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
+ return node instanceof DiagramNode;
+}
+
+function handleUploadError(error: HttpError, context: EditorUiContext): void {
+ if (error.status === 413) {
+ window.$events.emit('error', context.options.translations.serverUploadLimitText || '');
+ } else {
+ window.$events.emit('error', context.options.translations.imageUploadErrorText || '');
+ }
+ console.error(error);
+}
+
+async function loadDiagramIdFromNode(editor: LexicalEditor, node: DiagramNode): Promise {
+ const drawingId = await new Promise((res, rej) => {
+ editor.getEditorState().read(() => {
+ const {id: drawingId} = node.getDrawingIdAndUrl();
+ res(drawingId);
+ });
+ });
+
+ return drawingId || '';
+}
+
+async function updateDrawingNodeFromData(context: EditorUiContext, node: DiagramNode, pngData: string, isNew: boolean): Promise {
+ DrawIO.close();
+
+ if (isNew) {
+ const loadingImage: string = window.baseUrl('/loading.gif');
+ context.editor.update(() => {
+ node.setDrawingIdAndUrl('', loadingImage);
+ });
+ }
+
+ try {
+ const img = await DrawIO.upload(pngData, context.options.pageId);
+ context.editor.update(() => {
+ node.setDrawingIdAndUrl(String(img.id), img.url);
+ });
+ } catch (err) {
+ if (err instanceof HttpError) {
+ handleUploadError(err, context);
+ }
+
+ if (isNew) {
+ context.editor.update(() => {
+ node.remove();
+ });
+ }
+
+ throw new Error(`Failed to save image with error: ${err}`);
+ }
+}
+
+export function $openDrawingEditorForNode(context: EditorUiContext, node: DiagramNode): void {
+ let isNew = false;
+ DrawIO.show(context.options.drawioUrl, async () => {
+ const drawingId = await loadDiagramIdFromNode(context.editor, node);
+ isNew = !drawingId;
+ return isNew ? '' : DrawIO.load(drawingId);
+ }, async (pngData: string) => {
+ return updateDrawingNodeFromData(context, node, pngData, isNew);
+ });
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/utils/images.ts b/resources/js/wysiwyg/utils/images.ts
new file mode 100644
index 000000000..89a4a60f0
--- /dev/null
+++ b/resources/js/wysiwyg/utils/images.ts
@@ -0,0 +1,26 @@
+import {ImageManager} from "../../components";
+import {$createImageNode} from "../nodes/image";
+import {$createLinkNode, LinkNode} from "@lexical/link";
+
+type EditorImageData = {
+ url: string;
+ thumbs?: {display: string};
+ name: string;
+};
+
+export function showImageManager(callback: (image: EditorImageData) => any) {
+ const imageManager: ImageManager = window.$components.first('image-manager') as ImageManager;
+ imageManager.show((image: EditorImageData) => {
+ callback(image);
+ }, 'gallery');
+}
+
+export function $createLinkedImageNodeFromImageData(image: EditorImageData): LinkNode {
+ const url = image.thumbs?.display || image.url;
+ const linkNode = $createLinkNode(url, {target: '_blank'});
+ const imageNode = $createImageNode(url, {
+ alt: image.name
+ });
+ linkNode.append(imageNode);
+ return linkNode;
+}
\ No newline at end of file
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index 0cf145559..379c436f4 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -479,6 +479,16 @@ textarea.editor-form-field-input {
.editor-form-tab-contents {
width: 360px;
}
+.editor-action-input-container {
+ display: flex;
+ flex-direction: row;
+ align-items: end;
+ justify-content: space-between;
+ gap: .1rem;
+ .editor-button {
+ margin-bottom: 12px;
+ }
+}
// Editor theme styles
.editor-theme-bold {