Ngx-quill: Custom Modules

Created on 18 Feb 2018  Â·  5Comments  Â·  Source: KillerCodeMonkey/ngx-quill

It would be great to have an @Input variable to register custom modules at the Quill instance.

Implementation Possibility:
1. creating CustomModule interface
- name (e.g. 'module/custom-module')
- module object

  1. adding @Input variable

  2. registering all modules to quill instance

Or is there any other known way how to add a custom module to the ngx-quill component?

Most helpful comment

i got it - you should not use the compiled dist file of markdown shortcut.

use the /src/index.js

so there is no npm and special support - i combined everything of markdown shortcuts:

// Quill.js Plugin - Markdown Shortcuts
 // This is a module for the Quill.js WYSIWYG editor (https://quilljs.com/)
 // which converts text entered as markdown to rich text.
 //
 // v0.0.4
 //
 // Author: Patrick Lee ([email protected])
 //
 // (c) Copyright 2017 Patrick Lee ([email protected]).
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
 // in the Software without restriction, including without limitation the rights
 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 // copies of the Software, and to permit persons to whom the Software is
 // furnished to do so, subject to the following conditions:
 //
 // The above copyright notice and this permission notice shall be included in
 // all copies or substantial portions of the Software.
 //
 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 //
 import Quill from 'quill'
 let BlockEmbed = Quill.import('blots/block/embed')

 class HorizontalRule extends BlockEmbed {}
 HorizontalRule.blotName = 'hr'
 HorizontalRule.tagName = 'hr'

 Quill.register('formats/horizontal', HorizontalRule)

 class MarkdownShortcuts {
   constructor (quill, options) {
     this.quill = quill
     this.options = options

     this.matches = [
       {
         name: 'header',
         pattern: /^(#){1,6}\s/g,
         action: (text, selection) => {
           const size = text.trim().length
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 0, 'header', size)
             this.quill.deleteText(selection.index - text.length, text.length)
           }, 0)
         }
       },
       {
         name: 'blockquote',
         pattern: /^(>)\s/g,
         action: (text, selection) => {
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'blockquote', true)
             this.quill.deleteText(selection.index - 2, 2)
           }, 0)
         }
       },
       {
         name: 'code-block',
         pattern: /^`{3}(?:\s|\n)/g,
         action: (text, selection) => {
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'code-block', true)
             this.quill.deleteText(selection.index - 4, 4)
           }, 0)
         }
       },
       {
         name: 'bolditalic',
         pattern: /(?:\*|_){3}(.+?)(?:\*|_){3}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {bold: true})
             this.quill.format('bold', false)
           }, 0)
         }
       },
       {
         name: 'bold',
         pattern: /(?:\*|_){2}(.+?)(?:\*|_){2}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {bold: true})
             this.quill.format('bold', false)
           }, 0)
         }
       },
       {
         name: 'italic',
         pattern: /(?:\*|_){1}(.+?)(?:\*|_){1}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {italic: true})
             this.quill.format('italic', false)
           }, 0)
         }
       },
       {
         name: 'strikethrough',
         pattern: /(?:~~)(.+?)(?:~~)/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {strike: true})
             this.quill.format('strike', false)
           }, 0)
         }
       },
       {
         name: 'code',
         pattern: /(?:`)(.+?)(?:`)/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {code: true})
             this.quill.format('code', false)
             this.quill.insertText(this.quill.getSelection(), ' ')
           }, 0)
         }
       },
       {
         name: 'hr',
         pattern: /^([-*]\s?){3}/g,
         action: (text, selection) => {
           const startIndex = selection.index - text.length
           setTimeout(() => {
             this.quill.deleteText(startIndex, text.length)

             this.quill.insertEmbed(startIndex + 1, 'hr', true, Quill.sources.USER)
             this.quill.insertText(startIndex + 2, "\n", Quill.sources.SILENT)
             this.quill.setSelection(startIndex + 2, Quill.sources.SILENT)
           }, 0)
         }
       },
       {
         name: 'asterisk-ul',
         pattern: /^(\*|\+)\s$/g,
         action: (text, selection, pattern) => {
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'list', 'unordered')
             this.quill.deleteText(selection.index - 2, 2)
           }, 0)
         }
       },
       {
         name: 'image',
         pattern: /(?:!\[(.+?)\])(?:\((.+?)\))/g,
         action: (text, selection, pattern) => {
           const startIndex = text.search(pattern)
           const matchedText = text.match(pattern)[0]
           // const hrefText = text.match(/(?:!\[(.*?)\])/g)[0]
           const hrefLink = text.match(/(?:\((.*?)\))/g)[0]
           const start = selection.index - matchedText.length - 1
           if (startIndex !== -1) {
             setTimeout(() => {
               this.quill.deleteText(start, matchedText.length)
               this.quill.insertEmbed(start, 'image', hrefLink.slice(1, hrefLink.length - 1))
             }, 0)
           }
         }
       },
       {
         name: 'link',
         pattern: /(?:\[(.+?)\])(?:\((.+?)\))/g,
         action: (text, selection, pattern) => {
           const startIndex = text.search(pattern)
           const matchedText = text.match(pattern)[0]
           const hrefText = text.match(/(?:\[(.*?)\])/g)[0]
           const hrefLink = text.match(/(?:\((.*?)\))/g)[0]
           const start = selection.index - matchedText.length - 1
           if (startIndex !== -1) {
             setTimeout(() => {
               this.quill.deleteText(start, matchedText.length)
               this.quill.insertText(start, hrefText.slice(1, hrefText.length - 1), 'link', hrefLink.slice(1, hrefLink.length - 1))
             }, 0)
           }
         }
       }
     ]

     // Handler that looks for insert deltas that match specific characters
     this.quill.on('text-change', (delta, oldContents, source) => {
       for (let i = 0; i < delta.ops.length; i++) {
         if (delta.ops[i].hasOwnProperty('insert')) {
           if (delta.ops[i].insert === ' ') {
             this.onSpace()
           } else if (delta.ops[i].insert === '\n') {
             this.onEnter()
           }
         }
       }
     })
   }

   onSpace () {
     const selection = this.quill.getSelection()
     if (!selection) return
     const [line, offset] = this.quill.getLine(selection.index)
     const text = line.domNode.textContent
     const lineStart = selection.index - offset
     if (typeof text !== 'undefined' && text) {
       for (let match of this.matches) {
         const matchedText = text.match(match.pattern)
         if (matchedText) {
           // We need to replace only matched text not the whole line
           const textToReplace = matchedText[0]
           console.log('matched', match.name, textToReplace)
           match.action(textToReplace, selection, match.pattern, lineStart)
           return
         }
       }
     }
   }

   onEnter () {
     let selection = this.quill.getSelection()
     if (!selection) return
     const [line, offset] = this.quill.getLine(selection.index)
     const text = line.domNode.textContent + ' '
     const lineStart = selection.index - offset
     selection.length = selection.index++
     if (typeof text !== 'undefined' && text) {
       for (let match of this.matches) {
         const matchedText = text.match(match.pattern)
         if (matchedText) {
           console.log('matched', match.name, text)
           match.action(text, selection, match.pattern, lineStart)
           return
         }
       }
     }
   }
 }
 export default MarkdownShortcuts

and added it to my component:

import Quill from 'quill';
import Markdown from './quill-markdown-shortcut.js'
Quill.register('modules/markdownShortcuts', Markdown);

All 5 comments

What would be the advantage of this? I only know the "Quill.register"
functionality and this changes the global quill object. So I think it is
cleaner to register custom modules with the "Quill" way to do it. A
component should be an encapsulated piece of code. So I would not expect
that if I pass something as input to a component to have side effects to
other quill instances. If you know a "local" way to do it.. Let me know or
create a pull request :).

Thanks for your interest in this little repo.

Am 18.02.2018 16:48 schrieb "Markus Leimer" notifications@github.com:

It would be great to have an @input https://github.com/input variable
to register custom modules at the Quill instance.

Implementation Possibility:

  1. creating CustomModule interface

  2. name (e.g. 'module/custom-module')

  3. module object

    1.

    adding @input https://github.com/input variable
    2.

    registering all modules to quill instance

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/KillerCodeMonkey/ngx-quill/issues/114, or mute the
thread
https://github.com/notifications/unsubscribe-auth/ACKOYDPu5sNHbl9ZXswztSMNbyh1H2lXks5tWEYBgaJpZM4SJvAs
.

Thank you for your fast response,

you are right, I haven't thought about this unexpected behaviour concerning other quill instances.

The reason I opened this issue is that I can't get the Markdown Shortcuts Module to work.

I imported the js file:
import * as MarkdownShortcuts from '../../../assets/quill-markdown-shortcuts/dist/markdownShortcuts.js';

registered it in ngOnInit:
Quill.register('modules/markdownShortcuts', MarkdownShortcuts);

and added it to the editor options:
modules: { toolbar: [...], markdownShortcuts: {} },

but I get an error:
quill Cannot import modules/markdownShortcuts. Are you sure it was registered?

Did I miss anything?

Thank you for your help, and this awesome repo!

I will try to check this later :)

i got it - you should not use the compiled dist file of markdown shortcut.

use the /src/index.js

so there is no npm and special support - i combined everything of markdown shortcuts:

// Quill.js Plugin - Markdown Shortcuts
 // This is a module for the Quill.js WYSIWYG editor (https://quilljs.com/)
 // which converts text entered as markdown to rich text.
 //
 // v0.0.4
 //
 // Author: Patrick Lee ([email protected])
 //
 // (c) Copyright 2017 Patrick Lee ([email protected]).
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
 // in the Software without restriction, including without limitation the rights
 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 // copies of the Software, and to permit persons to whom the Software is
 // furnished to do so, subject to the following conditions:
 //
 // The above copyright notice and this permission notice shall be included in
 // all copies or substantial portions of the Software.
 //
 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 //
 import Quill from 'quill'
 let BlockEmbed = Quill.import('blots/block/embed')

 class HorizontalRule extends BlockEmbed {}
 HorizontalRule.blotName = 'hr'
 HorizontalRule.tagName = 'hr'

 Quill.register('formats/horizontal', HorizontalRule)

 class MarkdownShortcuts {
   constructor (quill, options) {
     this.quill = quill
     this.options = options

     this.matches = [
       {
         name: 'header',
         pattern: /^(#){1,6}\s/g,
         action: (text, selection) => {
           const size = text.trim().length
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 0, 'header', size)
             this.quill.deleteText(selection.index - text.length, text.length)
           }, 0)
         }
       },
       {
         name: 'blockquote',
         pattern: /^(>)\s/g,
         action: (text, selection) => {
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'blockquote', true)
             this.quill.deleteText(selection.index - 2, 2)
           }, 0)
         }
       },
       {
         name: 'code-block',
         pattern: /^`{3}(?:\s|\n)/g,
         action: (text, selection) => {
           // Need to defer this action https://github.com/quilljs/quill/issues/1134
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'code-block', true)
             this.quill.deleteText(selection.index - 4, 4)
           }, 0)
         }
       },
       {
         name: 'bolditalic',
         pattern: /(?:\*|_){3}(.+?)(?:\*|_){3}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {bold: true})
             this.quill.format('bold', false)
           }, 0)
         }
       },
       {
         name: 'bold',
         pattern: /(?:\*|_){2}(.+?)(?:\*|_){2}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {bold: true})
             this.quill.format('bold', false)
           }, 0)
         }
       },
       {
         name: 'italic',
         pattern: /(?:\*|_){1}(.+?)(?:\*|_){1}/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {italic: true})
             this.quill.format('italic', false)
           }, 0)
         }
       },
       {
         name: 'strikethrough',
         pattern: /(?:~~)(.+?)(?:~~)/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {strike: true})
             this.quill.format('strike', false)
           }, 0)
         }
       },
       {
         name: 'code',
         pattern: /(?:`)(.+?)(?:`)/g,
         action: (text, selection, pattern, lineStart) => {
           let match = pattern.exec(text)

           const annotatedText = match[0]
           const matchedText = match[1]
           const startIndex = lineStart + match.index

           if (text.match(/^([*_ \n]+)$/g)) return

           setTimeout(() => {
             this.quill.deleteText(startIndex, annotatedText.length)
             this.quill.insertText(startIndex, matchedText, {code: true})
             this.quill.format('code', false)
             this.quill.insertText(this.quill.getSelection(), ' ')
           }, 0)
         }
       },
       {
         name: 'hr',
         pattern: /^([-*]\s?){3}/g,
         action: (text, selection) => {
           const startIndex = selection.index - text.length
           setTimeout(() => {
             this.quill.deleteText(startIndex, text.length)

             this.quill.insertEmbed(startIndex + 1, 'hr', true, Quill.sources.USER)
             this.quill.insertText(startIndex + 2, "\n", Quill.sources.SILENT)
             this.quill.setSelection(startIndex + 2, Quill.sources.SILENT)
           }, 0)
         }
       },
       {
         name: 'asterisk-ul',
         pattern: /^(\*|\+)\s$/g,
         action: (text, selection, pattern) => {
           setTimeout(() => {
             this.quill.formatLine(selection.index, 1, 'list', 'unordered')
             this.quill.deleteText(selection.index - 2, 2)
           }, 0)
         }
       },
       {
         name: 'image',
         pattern: /(?:!\[(.+?)\])(?:\((.+?)\))/g,
         action: (text, selection, pattern) => {
           const startIndex = text.search(pattern)
           const matchedText = text.match(pattern)[0]
           // const hrefText = text.match(/(?:!\[(.*?)\])/g)[0]
           const hrefLink = text.match(/(?:\((.*?)\))/g)[0]
           const start = selection.index - matchedText.length - 1
           if (startIndex !== -1) {
             setTimeout(() => {
               this.quill.deleteText(start, matchedText.length)
               this.quill.insertEmbed(start, 'image', hrefLink.slice(1, hrefLink.length - 1))
             }, 0)
           }
         }
       },
       {
         name: 'link',
         pattern: /(?:\[(.+?)\])(?:\((.+?)\))/g,
         action: (text, selection, pattern) => {
           const startIndex = text.search(pattern)
           const matchedText = text.match(pattern)[0]
           const hrefText = text.match(/(?:\[(.*?)\])/g)[0]
           const hrefLink = text.match(/(?:\((.*?)\))/g)[0]
           const start = selection.index - matchedText.length - 1
           if (startIndex !== -1) {
             setTimeout(() => {
               this.quill.deleteText(start, matchedText.length)
               this.quill.insertText(start, hrefText.slice(1, hrefText.length - 1), 'link', hrefLink.slice(1, hrefLink.length - 1))
             }, 0)
           }
         }
       }
     ]

     // Handler that looks for insert deltas that match specific characters
     this.quill.on('text-change', (delta, oldContents, source) => {
       for (let i = 0; i < delta.ops.length; i++) {
         if (delta.ops[i].hasOwnProperty('insert')) {
           if (delta.ops[i].insert === ' ') {
             this.onSpace()
           } else if (delta.ops[i].insert === '\n') {
             this.onEnter()
           }
         }
       }
     })
   }

   onSpace () {
     const selection = this.quill.getSelection()
     if (!selection) return
     const [line, offset] = this.quill.getLine(selection.index)
     const text = line.domNode.textContent
     const lineStart = selection.index - offset
     if (typeof text !== 'undefined' && text) {
       for (let match of this.matches) {
         const matchedText = text.match(match.pattern)
         if (matchedText) {
           // We need to replace only matched text not the whole line
           const textToReplace = matchedText[0]
           console.log('matched', match.name, textToReplace)
           match.action(textToReplace, selection, match.pattern, lineStart)
           return
         }
       }
     }
   }

   onEnter () {
     let selection = this.quill.getSelection()
     if (!selection) return
     const [line, offset] = this.quill.getLine(selection.index)
     const text = line.domNode.textContent + ' '
     const lineStart = selection.index - offset
     selection.length = selection.index++
     if (typeof text !== 'undefined' && text) {
       for (let match of this.matches) {
         const matchedText = text.match(match.pattern)
         if (matchedText) {
           console.log('matched', match.name, text)
           match.action(text, selection, match.pattern, lineStart)
           return
         }
       }
     }
   }
 }
 export default MarkdownShortcuts

and added it to my component:

import Quill from 'quill';
import Markdown from './quill-markdown-shortcut.js'
Quill.register('modules/markdownShortcuts', Markdown);

Awesome thank you so much!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

swymmwys picture swymmwys  Â·  5Comments

patrickbattisti picture patrickbattisti  Â·  4Comments

theCrius picture theCrius  Â·  3Comments

Fzwael picture Fzwael  Â·  3Comments

agam16 picture agam16  Â·  5Comments