Browse Source

feat(admin-ui): Improve styling of rich text editor

Michael Bromley 3 years ago
parent
commit
054aba4d88

+ 2 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/images.ts

@@ -9,12 +9,13 @@ import {
     ExternalImageDialogComponent,
 } from '../../external-image-dialog/external-image-dialog.component';
 
-import { canInsert } from './menu-common';
+import { canInsert, renderClarityIcon } from './menu-common';
 
 export function insertImageItem(nodeType: NodeType, modalService: ModalService) {
     return new MenuItem({
         title: 'Insert image',
         label: 'Image',
+        render: renderClarityIcon({ shape: 'image', label: 'Image' }),
         class: '',
         css: '',
         enable(state: EditorState) {

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/links.ts

@@ -6,7 +6,7 @@ import { EditorState, TextSelection } from 'prosemirror-state';
 import { ModalService } from '../../../../../providers/modal/modal.service';
 import { LinkAttrs, LinkDialogComponent } from '../../link-dialog/link-dialog.component';
 
-import { markActive } from './menu-common';
+import { markActive, renderClarityIcon } from './menu-common';
 
 function selectionIsWithinLink(state: EditorState, anchor: number, head: number): boolean {
     const { doc } = state;
@@ -27,7 +27,7 @@ function selectionIsWithinLink(state: EditorState, anchor: number, head: number)
 export function linkItem(linkMark: MarkType, modalService: ModalService) {
     return new MenuItem({
         title: 'Add or remove link',
-        icon: icons.link,
+        render: renderClarityIcon({ shape: 'link', size: 22 }),
         class: '',
         css: '',
         active(state) {

+ 31 - 0
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu-common.ts

@@ -1,5 +1,7 @@
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { NodeType } from 'prosemirror-model';
 import { EditorState } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
 
 export function markActive(state, type) {
     const { from, $from, to, empty } = state.selection;
@@ -20,3 +22,32 @@ export function canInsert(state: EditorState, nodeType: NodeType): boolean {
     }
     return false;
 }
+
+export interface ClarityIconOptions {
+    shape: string;
+    size?: number;
+    label?: string;
+}
+
+export function renderClarityIcon(options: ClarityIconOptions): (view: EditorView) => HTMLElement {
+    return (view: EditorView) => {
+        const icon = document.createElement('clr-icon');
+        icon.setAttribute('shape', options.shape);
+        icon.setAttribute('size', (options.size ?? IconSize.Small).toString());
+        const labelEl = document.createElement('span');
+        labelEl.textContent = options.label ?? '';
+        return wrapInMenuItemWithIcon(icon, options.label ? labelEl : undefined);
+    };
+}
+
+export function wrapInMenuItemWithIcon(...elements: Array<HTMLElement | undefined | null>) {
+    const wrapperEl = document.createElement('span');
+    wrapperEl.classList.add('menu-item-with-icon');
+    wrapperEl.append(...elements.filter(notNullOrUndefined));
+    return wrapperEl;
+}
+
+export const IconSize = {
+    Large: 22,
+    Small: 16,
+};

+ 61 - 25
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu.ts

@@ -1,15 +1,13 @@
 import { toggleMark } from 'prosemirror-commands';
+import { redo, undo } from 'prosemirror-history';
 import {
     blockTypeItem,
     Dropdown,
-    DropdownSubmenu,
     icons,
     joinUpItem,
     liftItem,
     MenuItem,
-    redoItem,
     selectParentNodeItem,
-    undoItem,
     wrapItem,
 } from 'prosemirror-menu';
 import { MarkType, NodeType, Schema } from 'prosemirror-model';
@@ -20,15 +18,21 @@ import { ModalService } from '../../../../../providers/modal/modal.service';
 
 import { insertImageItem } from './images';
 import { linkItem } from './links';
-import { canInsert, markActive } from './menu-common';
+import { canInsert, IconSize, markActive, renderClarityIcon, wrapInMenuItemWithIcon } from './menu-common';
+import { SubMenuWithIcon } from './sub-menu-with-icon';
 import { addTable, getTableMenu } from './tables';
 
 // Helpers to create specific types of items
 
-function cmdItem(cmd: (...args: any[]) => void, options: Record<string, any>) {
+type CmdItemOptions = Record<string, any> & { iconShape?: string };
+
+function cmdItem(cmd: (...args: any[]) => void, options: CmdItemOptions) {
     const passedOptions = {
         label: options.title,
         run: cmd,
+        render: options.iconShape
+            ? renderClarityIcon({ shape: options.iconShape, size: IconSize.Large })
+            : undefined,
     };
     // tslint:disable-next-line:forin
     for (const prop in options) {
@@ -41,7 +45,7 @@ function cmdItem(cmd: (...args: any[]) => void, options: Record<string, any>) {
     return new MenuItem(passedOptions as any);
 }
 
-function markItem(markType, options) {
+function markItem(markType, options: CmdItemOptions) {
     const passedOptions = {
         active(state) {
             return markActive(state, markType);
@@ -55,7 +59,7 @@ function markItem(markType, options) {
     return cmdItem(toggleMark(markType), passedOptions);
 }
 
-function wrapListItem(nodeType, options) {
+function wrapListItem(nodeType, options: CmdItemOptions) {
     return cmdItem(wrapInList(nodeType, options.attrs), options);
 }
 
@@ -122,10 +126,16 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
     let type: MarkType | NodeType;
     // tslint:disable:no-conditional-assignment
     if ((type = schema.marks.strong)) {
-        r.toggleStrong = markItem(type, { title: 'Toggle strong style', icon: icons.strong });
+        r.toggleStrong = markItem(type, {
+            title: 'Toggle strong style',
+            iconShape: 'bold',
+        });
     }
     if ((type = schema.marks.em)) {
-        r.toggleEm = markItem(type, { title: 'Toggle emphasis', icon: icons.em });
+        r.toggleEm = markItem(type, {
+            title: 'Toggle emphasis',
+            iconShape: 'italic',
+        });
     }
     if ((type = schema.marks.code)) {
         r.toggleCode = markItem(type, { title: 'Toggle code font', icon: icons.code });
@@ -140,31 +150,31 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
     if ((type = schema.nodes.bullet_list)) {
         r.wrapBulletList = wrapListItem(type, {
             title: 'Wrap in bullet list',
-            icon: icons.bulletList,
+            iconShape: 'bullet-list',
         });
     }
     if ((type = schema.nodes.ordered_list)) {
         r.wrapOrderedList = wrapListItem(type, {
             title: 'Wrap in ordered list',
-            icon: icons.orderedList,
+            iconShape: 'number-list',
         });
     }
     if ((type = schema.nodes.blockquote)) {
         r.wrapBlockQuote = wrapItem(type, {
             title: 'Wrap in block quote',
-            icon: icons.blockquote,
+            render: renderClarityIcon({ shape: 'block-quote', size: IconSize.Large }),
         });
     }
     if ((type = schema.nodes.paragraph)) {
         r.makeParagraph = blockTypeItem(type, {
             title: 'Change to paragraph',
-            label: 'Plain',
+            render: renderClarityIcon({ shape: 'text', label: 'Plain' }),
         });
     }
     if ((type = schema.nodes.code_block)) {
         r.makeCodeBlock = blockTypeItem(type, {
             title: 'Change to code block',
-            label: 'Code',
+            render: renderClarityIcon({ shape: 'code', label: 'Code' }),
         });
     }
     if ((type = schema.nodes.heading)) {
@@ -180,9 +190,13 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
         const hr = type;
         r.insertHorizontalRule = new MenuItem({
             title: 'Insert horizontal rule',
-            label: 'Horizontal rule',
-            class: '',
-            css: '',
+            render: view => {
+                const icon = document.createElement('div');
+                icon.classList.add('custom-icon', 'hr-icon');
+                const labelEl = document.createElement('span');
+                labelEl.textContent = 'Horizontal rule';
+                return wrapInMenuItemWithIcon(icon, labelEl);
+            },
             enable(state) {
                 return canInsert(state, hr);
             },
@@ -198,7 +212,6 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
             r.insertImage,
             r.insertHorizontalRule,
             new MenuItem({
-                label: 'Add table',
                 run: (state, dispatch) => {
                     addTable(state, dispatch, {
                         rowsCount: 2,
@@ -207,6 +220,7 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
                         cellContent: '',
                     });
                 },
+                render: renderClarityIcon({ shape: 'table', label: 'Table' }),
             }),
         ]),
         { label: 'Insert' },
@@ -216,9 +230,17 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
             r.makeParagraph,
             r.makeCodeBlock,
             r.makeHead1 &&
-                new DropdownSubmenu(
+                new SubMenuWithIcon(
                     cut([r.makeHead1, r.makeHead2, r.makeHead3, r.makeHead4, r.makeHead5, r.makeHead6]),
-                    { label: 'Heading' },
+                    {
+                        label: 'Heading',
+                        icon: () => {
+                            const icon = document.createElement('div');
+                            icon.textContent = 'H';
+                            icon.classList.add('custom-icon', 'h-icon');
+                            return icon;
+                        },
+                    },
                 ),
         ]),
         { label: 'Type...' },
@@ -237,11 +259,25 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
             selectParentNodeItem,
         ]),
     ];
-    r.fullMenu = [inlineMenu].concat(
-        [[r.insertMenu, r.typeMenu, r.tableMenu]],
-        [[undoItem, redoItem]],
-        r.blockMenu,
-    );
+    const undoRedo = [
+        new MenuItem({
+            title: 'Undo last change',
+            run: undo,
+            enable(state) {
+                return undo(state);
+            },
+            render: renderClarityIcon({ shape: 'undo', size: IconSize.Large }),
+        }),
+        new MenuItem({
+            title: 'Redo last undone change',
+            run: redo,
+            enable(state) {
+                return redo(state);
+            },
+            render: renderClarityIcon({ shape: 'redo', size: IconSize.Large }),
+        }),
+    ];
+    r.fullMenu = [inlineMenu].concat([[r.insertMenu, r.typeMenu, r.tableMenu]], [undoRedo], r.blockMenu);
 
     return r;
 }

+ 29 - 0
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/sub-menu-with-icon.ts

@@ -0,0 +1,29 @@
+import { DropdownSubmenu, MenuElement } from 'prosemirror-menu';
+import { EditorState } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
+
+import { wrapInMenuItemWithIcon } from './menu-common';
+
+export class SubMenuWithIcon extends DropdownSubmenu {
+    private icon: HTMLElement;
+    constructor(
+        content: readonly MenuElement[] | MenuElement,
+        options: {
+            label?: string;
+            icon: () => HTMLElement;
+        },
+    ) {
+        super(content, options);
+        this.icon = options.icon();
+    }
+    render(view: EditorView): {
+        dom: HTMLElement;
+        update: (state: EditorState) => boolean;
+    } {
+        const { dom, update } = super.render(view);
+        return {
+            dom: wrapInMenuItemWithIcon(this.icon, dom),
+            update,
+        };
+    }
+}

+ 2 - 14
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/tables.ts

@@ -1,4 +1,4 @@
-import { buildMenuItems } from '@vendure/admin-ui/core';
+import { buildMenuItems, renderClarityIcon } from '@vendure/admin-ui/core';
 import OrderedMap from 'orderedmap';
 import { Dropdown, IconSpec, MenuElement, MenuItem } from 'prosemirror-menu';
 import { NodeSpec, Schema, Node } from 'prosemirror-model';
@@ -50,19 +50,7 @@ export function getTableMenu(schema: Schema) {
             label,
             select: cmd,
             run: cmd,
-            render: iconShape
-                ? view => {
-                      const icon = document.createElement('clr-icon');
-                      icon.setAttribute('shape', iconShape);
-                      icon.setAttribute('size', '16');
-                      const wrapperEl = document.createElement('span');
-                      wrapperEl.classList.add('menu-item-with-icon');
-                      const labelEl = document.createElement('span');
-                      labelEl.textContent = label;
-                      wrapperEl.append(icon, labelEl);
-                      return wrapperEl;
-                  }
-                : undefined,
+            render: iconShape ? renderClarityIcon({ shape: iconShape, label }) : undefined,
         });
     }
     function separator(): MenuElement {

+ 32 - 12
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.scss

@@ -73,13 +73,15 @@
 
     .ProseMirror-menuseparator {
         border-right: 1px solid var(--color-component-border-200);
-        margin-right: 3px;
+        margin: 0 12px 0 8px;
+        height: 18px;
     }
 
     .ProseMirror-menu-dropdown,
     .ProseMirror-menu-dropdown-menu {
         font-size: 90%;
         white-space: nowrap;
+        border-radius: var(--border-radius-input);
     }
 
     .ProseMirror-menu-dropdown {
@@ -90,7 +92,7 @@
     }
 
     .ProseMirror-menu-dropdown-wrap {
-        padding: 1px 0 1px 4px;
+        padding: 1px 3px 1px 6px;
         display: inline-block;
         position: relative;
     }
@@ -109,8 +111,7 @@
     .ProseMirror-menu-dropdown-menu,
     .ProseMirror-menu-submenu {
         position: absolute;
-        background: white;
-        color: var(--color-grey-600);
+        background: var(--color-component-bg-100);
         border: 1px solid var(--color-component-border-200);
         padding: 2px;
     }
@@ -118,6 +119,7 @@
     .ProseMirror-menu-dropdown-menu {
         z-index: 15;
         min-width: 6em;
+        color: var(--color-text-200);
     }
 
     .ProseMirror-menu-dropdown-item {
@@ -126,12 +128,12 @@
     }
 
     .ProseMirror-menu-dropdown-item:hover {
-        background: var(--color-component-bg-100);
+        background: var(--color-component-bg-200);
     }
 
     .ProseMirror-menu-submenu-wrap {
         position: relative;
-        margin-right: -4px;
+        margin-right: 4px;
     }
 
     .ProseMirror-menu-submenu-label:after {
@@ -141,7 +143,7 @@
         border-left: 4px solid currentColor;
         opacity: 0.6;
         position: absolute;
-        right: 4px;
+        right: -8px;
         top: calc(50% - 4px);
     }
 
@@ -186,6 +188,7 @@
         -moz-box-sizing: border-box;
         box-sizing: border-box;
         overflow: visible;
+        align-items: center;
     }
 
     .ProseMirror-icon {
@@ -338,14 +341,17 @@
     }
 
     .ProseMirror .tableWrapper {
-        td, th {
+        td,
+        th {
             border: 1px solid var(--color-grey-400);
             padding: 3px 6px;
         }
-        td p, th p {
+        td p,
+        th p {
             margin-top: 0;
         }
-        th, th p {
+        th,
+        th p {
             font-weight: bold;
         }
     }
@@ -359,8 +365,22 @@
     .menu-item-with-icon {
         display: flex;
         align-items: center;
-        clr-icon {
-            margin-right: 2px;
+        clr-icon,
+        .custom-icon {
+            margin-right: 4px;
+            color: var(--color-text-200);
+        }
+        .hr-icon {
+            width: 13px;
+            height: 8px;
+            border-bottom: 2px solid var(--color-text-100);
+            margin: -8px 5px 0 2px;
+        }
+        .h-icon {
+            width: 16px;
+            text-align: center;
+            font-weight: bold;
+            font-size: 12px;
         }
     }
 }