user-card.ts 5.75 KB
import { Boot, DomEditor, IButtonMenu, IDomEditor, SlateElement, SlateText, SlateTransforms } from '@wangeditor/editor';

import { h, VNode } from 'snabbdom';
import { createVNode, defineAsyncComponent } from 'vue';
import { Node } from 'slate';
import { Modal } from '@arco-design/web-vue';

const MENU = {
  KEY: 'user-card',
  TITLE: '用户',
  ICON: '<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" class="arco-icon arco-icon-user" stroke-width="4" stroke-linecap="butt" stroke-linejoin="miter"><path d="M7 37c0-4.97 4.03-8 9-8h16c4.97 0 9 3.03 9 8v3a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-3Z"></path><circle cx="24" cy="15" r="8"></circle></svg>',
};

const DELETE_ICON = 'https://hi-sing-cdn.hikoon.com/image/20230818/tajefbycbk71692325471868yswxs35mag.png';

type UserCardElement = { type: 'user-card'; id: number; nick_name: string; avatar: string; children: SlateText[] };

function withCard<T extends IDomEditor>(editor: T) {
  const { isVoid, isInline } = editor;

  const newEditor = editor;

  newEditor.isVoid = (elem) => {
    const type = DomEditor.getNodeType(elem);
    if (type === MENU.KEY) {
      return true;
    }

    return isVoid(elem);
  };

  newEditor.isInline = (elem) => {
    const type = DomEditor.getNodeType(elem);
    if (type === MENU.KEY) {
      return true;
    }

    return isInline(elem);
  };

  return newEditor;
}

function createCard(editor: IDomEditor, callback?: () => void) {
  const UserTable = defineAsyncComponent(() => import('@/views/operation/banner/components/user-table.vue'));

  return createVNode(UserTable, {
    onCheck: (value: any) => {
      const { link_id, link_name, cover } = value;
      editor.insertNode({ type: MENU.KEY, id: link_id, nick_name: link_name, avatar: cover, children: [{ text: '' }] } as Node);
      callback?.();
    },
  });
}

class UserCard implements IButtonMenu {
  title = MENU.TITLE;

  iconSvg = MENU.ICON;

  tag = 'button';

  getValue(): boolean {
    return false;
  }

  isActive(): boolean {
    return false;
  }

  isDisabled(): boolean {
    return false;
  }

  // 点击菜单时触发的函数
  exec(editor: IDomEditor) {
    const modal = Modal.open({
      title: MENU.TITLE,
      content: () => createCard(editor, () => modal?.close()),
      width: '700px',
      footer: false,
      closable: true,
      escToClose: true,
      // @ts-ignore
      bodyStyle: { padding: '16px' },
    });
  }
}

const menuConf = { key: MENU.KEY, factory: () => new UserCard() };

const renderElemConf = {
  type: MENU.KEY,
  renderElem: (elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode => {
    const { nick_name, avatar } = elem as UserCardElement;

    return h(
      'span',
      {
        props: { contentEditable: false },
        style: {
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          background: '#f2f2f2',
          padding: '8px',
          border: '1px solid rgb(229,230,235)',
          boxShadow: '0 2px 5px 0 rgba(0, 0, 0, 0.08)',
          maxWidth: '400px',
        },
      },
      [
        h('img', {
          props: { src: avatar, alt: '' },
          style: { width: '48px', height: '48px', borderRadius: '50%' },
        }),
        h(
          'span',
          {
            props: { contentEditable: false },
            style: {
              flex: '1',
              margin: '0 6px',
              overflow: 'hidden',
              whiteSpace: 'nowrap',
              textOverflow: 'ellipsis',
              fontWeight: '500',
              color: '#4E5969',
            },
          },
          nick_name
        ),
        h('img', {
          props: { src: DELETE_ICON, contentEditable: false },
          style: { width: '22px', visibility: editor.isDisabled() ? 'hidden' : 'visibility' },
          on: {
            click() {
              // eslint-disable-next-line func-names
              setTimeout(function () {
                const node = DomEditor.getSelectedNodeByType(editor, MENU.KEY);
                if (node) {
                  const path = DomEditor.findPath(editor, node);
                  SlateTransforms.removeNodes(editor, { at: path });
                }
              }, 150);
            },
          },
        }),
      ]
    );
  },
};

const parseHtmlConf = {
  selector: `span[data-w-e-type="${MENU.KEY}"]`,
  parseElemHtml: (domElem: Element): SlateElement => {
    const id = domElem.getAttribute('data-id') || 0;
    const nick_name = domElem.getAttribute('data-nick-name') || '';
    const avatar = domElem.getAttribute('data-avatar') || '';
    return { type: MENU.KEY, id, nick_name, avatar, children: [{ text: '' }] } as SlateElement;
  },
};

const elemToHtmlConf = {
  type: MENU.KEY,
  elemToHtml: (elem: SlateElement) => {
    const { id, nick_name, avatar } = elem as UserCardElement;
    return `<span style="display: flex;align-items:center;justify-content: space-between;background: #f2f2f2;padding: 8px;border:1px solid rgb(229,230,235);box-shadow:0 2px 5px 0 rgba(0, 0, 0, 0.08);margin: 4px" data-w-e-type="${MENU.KEY}" data-w-e-is-void data-w-e-is-inline data-id="${id}" data-nick-name="${nick_name}" data-avatar="${avatar}">
              <img src="${avatar}" style="width: 3em !important;height: 3em !important;object-fit:fill;border-radius:50%" alt="" />
              <span style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;flex: 1;margin: 0 6px;font-weight: 500;color: #4E5969">${nick_name}</span>
              <img src="https://hi-sing-cdn.hikoon.com/image/20230822/sjzhgel3pwo1692672120063rjd093t6xd.png" style="width: 22px" alt="" />
            </span>`;
  },
};

Boot.registerModule({
  editorPlugin: withCard,
  renderElems: [renderElemConf],
  elemsToHtml: [elemToHtmlConf],
  parseElemsHtml: [parseHtmlConf],
  menus: [menuConf],
});