Skip to content

Instantly share code, notes, and snippets.

@slava-vishnyakov
Last active June 9, 2025 07:55
Show Gist options
  • Save slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521 to your computer and use it in GitHub Desktop.
Save slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521 to your computer and use it in GitHub Desktop.

Revisions

  1. slava-vishnyakov revised this gist Nov 19, 2020. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -194,6 +194,7 @@ props: {
    });

    } else {
    const reader = new FileReader();
    reader.onload = readerEvent => {
    const node = schema.nodes.image.create({
    src: readerEvent.target.result,
  2. slava-vishnyakov revised this gist Jan 28, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -140,7 +140,7 @@ export default class Image extends Node {
    ```js
    import Image from './Image';

    async upload(file) {
    async function upload(file) {
    ...
    }

    @@ -154,7 +154,7 @@ new Editor({
    3) Implement the `upload` function:
    ```js
    async upload(file) {
    async function upload(file) {
    let formData = new FormData();
    formData.append('file', file);
    const headers = {'Content-Type': 'multipart/form-data'};
  3. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -177,12 +177,12 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    props: {
    handlePaste(view, event, slice) {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") === 0) {
    for (const item of items) {
    if (item.type.indexOf("image") === 0) {
    event.preventDefault();
    const { schema } = view.state;

    const image = items[i].getAsFile();
    const image = item.getAsFile();

    if(upload) {
    upload(image).then(src => {
  4. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -207,6 +207,6 @@ props: {
    }
    }
    return false;
    }
    },
    handleDOMEvents: {
    ```
  5. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 11 additions and 7 deletions.
    18 changes: 11 additions & 7 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -171,11 +171,11 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    5) Implement server-side logic for `/upload`
    6) (!! This doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    6) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
    async handlePaste(view, event, slice) {
    handlePaste(view, event, slice) {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") === 0) {
    @@ -185,11 +185,14 @@ props: {
    const image = items[i].getAsFile();

    if(upload) {
    const node = schema.nodes.image.create({
    src: await upload(image),
    upload(image).then(src => {
    const node = schema.nodes.image.create({
    src: src,
    });
    const transaction = view.state.tr.replaceSelectionWith(node);
    view.dispatch(transaction)
    });
    const transaction = view.state.tr.replaceSelectionWith(node);
    view.dispatch(transaction)

    } else {
    reader.onload = readerEvent => {
    const node = schema.nodes.image.create({
    @@ -203,6 +206,7 @@ props: {

    }
    }
    },
    return false;
    }
    handleDOMEvents: {
    ```
  6. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -171,7 +171,7 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    5) Implement server-side logic for `/upload`
    6) (/!\ This doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    6) (!! This doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
  7. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -171,7 +171,7 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    5) Implement server-side logic for `/upload`
    6) ((!) Doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    6) (/!\ This doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
  8. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -171,7 +171,7 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    5) Implement server-side logic for `/upload`
    6) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    6) ((!) Doesn't work fully yet, prevents paste of text) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
  9. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -171,7 +171,7 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    5) Implement server-side logic for `/upload`
    6) If you want to support pasting of images, modify `Image.js` startings at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    6) If you want to support pasting of images, modify `Image.js` starting at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
  10. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -176,8 +176,8 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    ```js
    props: {
    async handlePaste(view, event, slice) {
    var items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (var i = 0; i < items.length; i++) {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") === 0) {
    event.preventDefault();
    const { schema } = view.state;
  11. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 37 additions and 1 deletion.
    38 changes: 37 additions & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -169,4 +169,40 @@ This `POST`s using `axios` to `/upload` and expects a JSON back of this form:
    {"src": "https://yoursite.com/images/uploadedimage.jpg"}
    ```
    5) Implement server-side logic for `/upload`
    5) Implement server-side logic for `/upload`
    6) If you want to support pasting of images, modify `Image.js` startings at `props:`, ending at `handleDOMEvents` (re-factor common parts if you want to)
    ```js
    props: {
    async handlePaste(view, event, slice) {
    var items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (var i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") === 0) {
    event.preventDefault();
    const { schema } = view.state;

    const image = items[i].getAsFile();

    if(upload) {
    const node = schema.nodes.image.create({
    src: await upload(image),
    });
    const transaction = view.state.tr.replaceSelectionWith(node);
    view.dispatch(transaction)
    } else {
    reader.onload = readerEvent => {
    const node = schema.nodes.image.create({
    src: readerEvent.target.result,
    });
    const transaction = view.state.tr.replaceSelectionWith(node);
    view.dispatch(transaction)
    };
    reader.readAsDataURL(image)
    }

    }
    }
    },
    handleDOMEvents: {
    ```
  12. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    1) Create a file `Image.js` from the source below (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded) and additional logic `if(upload) { ... } else { ... previous base64 logic .. }` in `new Plugin` section.
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded) and additional logic `if(upload) { ... } else { ... previous base64 logic .. }` in the `new Plugin` section.

    ```js
    import {Node, Plugin} from 'tiptap'
  13. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    1) Create a file `Image.js` from source below (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    1) Create a file `Image.js` from the source below (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded) and additional logic `if(upload) { ... } else { ... previous base64 logic .. }` in `new Plugin` section.

    ```js
  14. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 6 additions and 2 deletions.
    8 changes: 6 additions & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -137,17 +137,21 @@ export default class Image extends Node {

    2) Import it:

    ```
    ```js
    import Image from './Image';

    async upload(file) {
    ...
    }

    new Editor({
    extensions: [
    ...
    new Image(null, null, upload),
    ...
    ```
    3) Create `upload` function:
    3) Implement the `upload` function:
    ```js
    async upload(file) {
  15. slava-vishnyakov revised this gist Oct 24, 2019. No changes.
  16. slava-vishnyakov revised this gist Oct 24, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    1) Create a file Image.js (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded).
    1) Create a file `Image.js` from source below (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded) and additional logic `if(upload) { ... } else { ... previous base64 logic .. }` in `new Plugin` section.

    ```js
    import {Node, Plugin} from 'tiptap'
  17. slava-vishnyakov created this gist Oct 24, 2019.
    168 changes: 168 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,168 @@
    1) Create a file Image.js (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js)
    except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded).

    ```js
    import {Node, Plugin} from 'tiptap'
    import {nodeInputRule} from 'tiptap-commands'

    /**
    * Matches following attributes in Markdown-typed image: [, alt, src, title]
    *
    * Example:
    * ![Lorem](image.jpg) -> [, "Lorem", "image.jpg"]
    * ![](image.jpg "Ipsum") -> [, "", "image.jpg", "Ipsum"]
    * ![Lorem](image.jpg "Ipsum") -> [, "Lorem", "image.jpg", "Ipsum"]
    */
    const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;

    export default class Image extends Node {

    constructor(name, parent, uploadFunc = null) {
    super(name, parent);
    this.uploadFunc = uploadFunc;
    }

    get name() {
    return 'image'
    }

    get schema() {
    return {
    inline: true,
    attrs: {
    src: {},
    alt: {
    default: null,
    },
    title: {
    default: null,
    },
    },
    group: 'inline',
    draggable: true,
    parseDOM: [
    {
    tag: 'img[src]',
    getAttrs: dom => ({
    src: dom.getAttribute('src'),
    title: dom.getAttribute('title'),
    alt: dom.getAttribute('alt'),
    }),
    },
    ],
    toDOM: node => ['img', node.attrs],
    }
    }

    commands({ type }) {
    return attrs => (state, dispatch) => {
    const { selection } = state;
    const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos;
    const node = type.create(attrs);
    const transaction = state.tr.insert(position, node);
    dispatch(transaction)
    }
    }

    inputRules({ type }) {
    return [
    nodeInputRule(IMAGE_INPUT_REGEX, type, match => {
    const [, alt, src, title] = match;
    return {
    src,
    alt,
    title,
    }
    }),
    ]
    }

    get plugins() {
    const upload = this.uploadFunc;
    return [
    new Plugin({
    props: {
    handleDOMEvents: {
    drop(view, event) {
    const hasFiles = event.dataTransfer
    && event.dataTransfer.files
    && event.dataTransfer.files.length;

    if (!hasFiles) {
    return
    }

    const images = Array
    .from(event.dataTransfer.files)
    .filter(file => (/image/i).test(file.type));

    if (images.length === 0) {
    return
    }

    event.preventDefault();

    const { schema } = view.state;
    const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });

    images.forEach(async image => {
    const reader = new FileReader();

    if(upload) {
    const node = schema.nodes.image.create({
    src: await upload(image),
    });
    const transaction = view.state.tr.insert(coordinates.pos, node);
    view.dispatch(transaction)
    } else {
    reader.onload = readerEvent => {
    const node = schema.nodes.image.create({
    src: readerEvent.target.result,
    });
    const transaction = view.state.tr.insert(coordinates.pos, node);
    view.dispatch(transaction)
    };
    reader.readAsDataURL(image)
    }
    })
    },
    },
    },
    }),
    ]
    }

    }
    ```

    2) Import it:

    ```
    import Image from './Image';
    new Editor({
    extensions: [
    ...
    new Image(null, null, upload),
    ...
    ```

    3) Create `upload` function:

    ```js
    async upload(file) {
    let formData = new FormData();
    formData.append('file', file);
    const headers = {'Content-Type': 'multipart/form-data'};
    const response = await axios.post('/upload', formData, {headers: headers} );
    return response.data.src;
    },
    ```

    This `POST`s using `axios` to `/upload` and expects a JSON back of this form:

    ```
    {"src": "https://yoursite.com/images/uploadedimage.jpg"}
    ```

    5) Implement server-side logic for `/upload`