github.com/ngocphuongnb/tetua@v0.0.7-alpha/packages/editor/src/extensions/image.ts (about)

     1  import { Editor } from '@tiptap/core';
     2  import Image from '@tiptap/extension-image';
     3  import { createNodeViewBlock } from '../utils';
     4  
     5  export type ImageUploadHandler = (file: File, callback: (url: string, err?: Error) => void) => void;
     6  export interface ImageExtensionProps {
     7    uploadHandler?: ImageUploadHandler;
     8    disableTitle?: boolean;
     9  }
    10  
    11  const createImageUrlElm = (editor: Editor, getPos: boolean | (() => number)) => {
    12    const imageUrlContainer = document.createElement('div');
    13    const imageUrlInput = document.createElement('input');
    14    const imageUrlApplyBtn = document.createElement('button');
    15  
    16    imageUrlContainer.className = 'mely-editor-img-url';
    17    imageUrlApplyBtn.innerText = 'Insert';
    18    imageUrlInput.setAttribute('type', 'text');
    19    imageUrlInput.setAttribute('placeholder', 'Enter image URL');
    20    imageUrlApplyBtn.addEventListener('click', (e: KeyboardEvent) => {
    21      e.preventDefault();
    22      e.stopImmediatePropagation();
    23  
    24      if (!imageUrlInput.value) {
    25        imageUrlInput.focus();
    26        return;
    27      }
    28  
    29      if (typeof getPos === 'function') {
    30        editor.view.dispatch(editor.view.state.tr.setNodeMarkup(getPos(), undefined, {
    31          src: imageUrlInput.value,
    32          alt: '',
    33          title: '',
    34        }))
    35        editor.commands.focus();
    36      }
    37    });
    38  
    39    imageUrlContainer.append(imageUrlInput, imageUrlApplyBtn);
    40    setTimeout(() => imageUrlInput.focus(), 0);
    41  
    42    return imageUrlContainer;
    43  }
    44  
    45  const createOrTextElm = () => {
    46    const orText = document.createElement('p');
    47    orText.innerText = 'or';
    48    return orText;
    49  }
    50  
    51  const createImageUploadElm = (dom: HTMLDivElement, uploadHandler: ImageUploadHandler, editor: Editor, getPos: boolean | (() => number)) => {
    52    const uploadElm = document.createElement('div');
    53    const uploadInput = document.createElement('input');
    54    const uploadBtn = document.createElement('button');
    55  
    56    uploadElm.className = 'mely-editor-img-upload';
    57    uploadInput.setAttribute('type', 'file');
    58    uploadInput.setAttribute('accept', 'image/*');
    59    uploadInput.setAttribute('name', 'file');
    60    uploadInput.setAttribute('id', 'file');
    61    uploadInput.setAttribute('style', 'display: none;');
    62    uploadBtn.setAttribute('type', 'button');
    63    uploadBtn.setAttribute('class', 'mely-editor-img-upload-btn');
    64    uploadBtn.innerText = 'Select file';
    65    uploadBtn.addEventListener('click', (e) => {
    66      e.preventDefault();
    67      uploadInput.click();
    68    });
    69  
    70    uploadInput.addEventListener('change', (e) => {
    71      const target = e.target as HTMLInputElement;
    72      const file = target.files[0];
    73      dom.classList.add('uploading');
    74      uploadHandler(file, (url, err) => {
    75        dom.classList.remove('uploading');
    76        if (err) {
    77          console.error(err);
    78          alert('Upload failed');
    79          return;
    80        }
    81  
    82        if (typeof getPos === 'function') {
    83          editor.view.dispatch(editor.view.state.tr.setNodeMarkup(getPos(), undefined, {
    84            src: url,
    85            alt: '',
    86            title: '',
    87          }))
    88          editor.commands.focus();
    89        }
    90      });
    91    });
    92  
    93    uploadElm.append(uploadInput, uploadBtn);
    94    return uploadElm;
    95  }
    96  
    97  export const getImageExtension = (props: ImageExtensionProps = {}) => {
    98    const uploadHandler = props.uploadHandler || (() => console.log('Upload handler not set'));
    99  
   100    return Image.extend({
   101      onCreate() {
   102        window.addEventListener('paste', (e: ClipboardEvent) => {
   103          if (e.clipboardData.files && e.clipboardData.files.length > 0) {
   104            e.preventDefault();
   105            e.stopImmediatePropagation();
   106            e.stopPropagation();
   107            const file = e.clipboardData.files[0];
   108            const selection = this.editor.view.state.tr.selection;
   109            const pos = selection.$anchor.pos - 1;
   110  
   111            if (!props.disableTitle) {
   112              const position = selection.$from;
   113              let pastedOnTitleField = false;
   114              let titleNode = null;
   115    
   116              position.doc.nodesBetween(selection.from, selection.to, (node) => {
   117                titleNode = node;
   118                pastedOnTitleField = node.type.name === 'heading' && node.attrs.level == 1;
   119              });
   120  
   121              if (pastedOnTitleField) {
   122                alert('Can\'t paste image on title field');
   123                if (titleNode) {
   124                  this.editor.view.dispatch(this.editor.view.state.tr.setNodeMarkup(
   125                    pos,
   126                    this.editor.schema.nodes.paragraph
   127                  ));
   128                }
   129                return;
   130              }
   131            }
   132  
   133            uploadHandler(file, (url, err) => {
   134              if (err) {
   135                console.error(err);
   136                alert('Upload failed');
   137                this.editor.view.dispatch(this.editor.view.state.tr.setNodeMarkup(
   138                  pos,
   139                  this.editor.schema.nodes.paragraph
   140                ));
   141                return;
   142              }
   143  
   144              this.editor.view.dispatch(this.editor.view.state.tr.setNodeMarkup(pos, undefined, {
   145                src: url,
   146                alt: '',
   147                title: '',
   148              }));
   149            });
   150          }
   151        });
   152      },
   153      addNodeView() {
   154        return ({
   155          editor,
   156          node: _node,
   157          getPos,
   158          HTMLAttributes: attrs,
   159          decorations: _decorations,
   160          extension: _extension
   161        }) => {
   162  
   163          if (!props.disableTitle && typeof getPos === 'function') {
   164            let pastedOnTitleField = false
   165            const selection = editor.view.state.selection;
   166            const position = selection.$from;
   167            const pos = getPos()
   168            position.doc.nodesBetween(pos, pos, (node) => {
   169              pastedOnTitleField = node.type.name === 'heading' && node.attrs.level == 1;
   170            });
   171  
   172            if (pastedOnTitleField) {
   173              alert('Can\'t insert image on title field');
   174              return;
   175            }
   176          }
   177  
   178          const contentDomElm = document.createElement('img');
   179          contentDomElm.setAttribute('src', attrs.src);
   180          contentDomElm.setAttribute('alt', attrs.alt || '');
   181          contentDomElm.setAttribute('title', attrs.title || '');
   182  
   183          const { dom, view } = createNodeViewBlock(contentDomElm, []);
   184          dom.classList.add('block-img');
   185          view.append(
   186            createImageUrlElm(editor, getPos),
   187            createOrTextElm(),
   188            createImageUploadElm(dom, uploadHandler, editor, getPos)
   189          );
   190  
   191          return {
   192            dom,
   193            contentDOM: contentDomElm,
   194            stopEvent: () => !attrs.src,
   195            ignoreMutation: _mutation => !attrs.src,
   196          }
   197        }
   198      },
   199    }).configure({ inline: true });
   200  }