nodebb-plugin-composer-quill: WYSIWYG alternative to redactor

  • GNU/Linux Admin

    While I don't like to have individual plugins integrate with Quill, sometimes it is necessary. For example, many plugins detect whether Redactor is in use, and hook methods directly into Redactor.

    It is my hope that this will not be necessary with Quill, but one never knows 😄 -- with regard to the emoji in my screenshots, the emoji plugin works fine with Quill but only if you type in the :smile: code yourself. Otherwise it will not work as you discovered, because the plugin does not know how to insert text/images into the Quill contenteditable.

    I do not have time this month to work on Quill, but there is hopefully time scheduled in October 😄 I look forward to getting it to 100% compatibility so we can bundle it with NodeBB in addition to composer-default.


  • Getting this plugin integrated into composer is a very desirable target, I believe.

    If you need help with this, I can lend a hand. Do let me know if there are areas you'd like me to explore, so we'll get as much done by Oct.


  • Hi! How to install Quill Composer? I can't find it in NPM


  • Hi @Kosiak
    You'll have to clone it from here: https://github.com/NodeBB/nodebb-plugin-composer-quill
    Its not PnP, yet... 🙂 so get yourself ready for few code changes - no biggie, just follow the above.
    Good luck!
    JJ.


  • Hi @JJSagan
    Ohh, I see. I'm not a coder. I'm just starting to learn NodeBB. I'm looking for something to install through the admin area. Thx!


  • No problemo @Kosiak , do check back on this plugin after October.
    If you are looking for WYSIWYG then I believe this could be the solution for you, since Redactor unfortunately may be on its way out.
    Take care and good luck with your forum!
    btw: i started few months ago as none-coder like you (and yeah I still suck...), so be careful you might get addicted and convert 😉


  • Hi @PitaJ .

    I am trying to debug nodebb-plugin-emoji and make it work with quill.

    I cloned your depository, and compiled, however I can't see the sourcemaps of emoji-dialog.ts.

    I tried changing tsconfig.json to include: "inlineSourceMap": true, "inlineSources": true, but still no sourcemap. Interestingly, emoji-setup.js does have a sourcemap.

    Any idea what I am doing wrong?
    Thank you!
    JJ.


  • Hi @PitaJ

    Its difficult without a debugger, but I am quite confident the issue stem from showDeferred function - see init in emoji-dialog.js and look for getBoundingClientRect.
    Either tabContent[0] or elem (from callback) are undefined.

    The above code gets executed upon clicking the emoji icon in quill, that is, as a result of the dispatcher:

    formatting.addButtonDispatch('emoji-add-emoji', function (textarea) {
                    new Promise(function (resolve_4, reject_4) { require(['emoji-dialog'], resolve_4, reject_4); }).then(function (_a) {
                        var toggleForInsert = _a.toggleForInsert;
                        return toggleForInsert(textarea);
                    });
                });
    

    Console output:

    Failed to initialize emoji dialog TypeError: Cannot read property 'getBoundingClientRect' of undefined
        at n (emoji-dialog.js?v=q79j5o5953s:1)
        at emoji-dialog.js?v=q79j5o5953s:1
    

    I wonder if this gives you any clue with regards to a potential fix.
    JJ.

  • Global Moderator Plugin & Theme Dev

    @JJSagan I'll look into it and see if I can find a solution in the coming days.


  • Hi @julian and @PitaJ

    While reviewing the composer redactor code, I noted you are using a special set of emojis, rather than nodebb-plugin-emoji.

    To that end, I took another look at rendering the quill-emoji plugin (https://github.com/contentco/quill-emoji), that is, without using a deadbeat quill editor. See an earlier post how to install.

    The solution was to write a customized rendering code using converter.renderCustomWith(). See my suggested changes to library.js:

    plugin.parseRaw = function (raw, callback) {
            //Convert delta strings
    	if((raw != null) && (raw.hasOwnProperty("ops"))) {
    		raw = JSON.stringify(raw);
    	}
    
    	try {
    		var unescaped = raw.replace(/"/g, '"');
    		var content = JSON.parse(unescaped);
    		var converter = new QuillDeltaToHtmlConverter(content.ops, {});
    
    		converter.renderCustomWith(function(customOp, contextOp){
    			if (customOp.insert.type === 'emoji') {
    				return `<span class="${customOp.attributes.class}" src="${customOp.insert.value}"><span contenteditable="false"></span></span>`;
    			} else {
    				return 'Unmanaged custom blot!';
    			}
    		});
    
    		raw = converter.convert();
    	} catch (e) {
    		// Do nothing
    		winston.warn('[composer-quill] Input not in expected format, skipping. - ', raw);
    	}
    
    	callback(null, raw);
    };
    

    I'll play with it some more, but so far it looks solid.
    Perhaps its better to use quill-emoji (over nodebb-plugin-emoji), to insure long term compatibility with quill.

    JJ.

  • GNU/Linux Admin

    There's no reason you can't use quill's emoji, although as emoji plugin is bundled with NodeBB, it would definitely be better to use our own emoji plugin 😄


  • Hi @julian

    One disadvantage to quill-emoji is that they are using data-uri. Rendering the pack upon refresh is slow. The png file is huge and re-downloaded for some reason on each refresh. Offloading the png to S3 also proves to be very difficult (its currently under /public).

    It should be possible to integrate nodebb's plugin. I tried but its difficult to work without a debugger (for some reason I can't access the source map due to webpack config - see post above). Nevertheless I pinpointed the problematic code and asked @PitaJ for some help. Wish I had the source map available...

    JJ.

  • GNU/Linux Admin

    @JJSagan All things will be completed in time 😄 Just need to find the time is all 😓


  • Hi @PitaJ

    My apology, I can't figure out how to create source map in nodebb-plugin-emoji, specifically for emoji-dialog.js. Can you please help? I'd like to work-around the issue we are having with quill, so that @julian won't have to spend his time on this.

    A bit of guidance and i'll be on my way, scouts-honor! ✌ (no three finger emoji...).

    Thank you in advance!
    JJ.

  • Global Moderator Plugin & Theme Dev

    @JJSagan I never really needed the sourcemap file on the client side since the compiled unminified js code is essentially the same as the typescript source. Are .map files not being generated in the build directory alongside the compiled js files?


  • Hi @PitaJ ,

    The issue with source map was my bad: emoji-dialog.js is compiled as module and I was working in dev mode, so the code got minified by nodebb engine. Switched to prod, and able to work.

    Now for the goodies, in emoji-dialog.js kindly consider adding the following code in line 174:

    //if Quill
            if(document.querySelectorAll('button.ql-emoji-add-emoji').length)
                opener = (<HTMLScriptElement[]><any>document.querySelectorAll('button.ql-emoji-add-emoji'))[0];
    

    In a nutshell, opener is undefined when Quill is used, specifically because the emoji icon is placed on a different bar belonging to quill. The above code checks if Quill emoji selector exists, and if so properly assigns opener.

    nodebb-plugin-emoji is without a set, so I installed nodebb-plugin-emoji-one, and... we have emojis displayed in the modal. Next I need to figure out how to insert them into quill.

    Stay tuned.
    JJ.


  • Hi @julian , @PitaJ

    Finally got nodebb-plugin-emoji working on Quill.
    As an added bonus, the emojis can be used inside the post AND in the title bar.

    In emoji-dialog.js make the following changes, allowing quill to work side by side with composer.

    1. Top of file, add import:
    import { active as activeComposer } from 'composer';
    
    1. In toggle, update opener if quill is available:
    export function toggle(
        opener: HTMLElement,
        onClick: (e: JQuery.Event, name: string, dialog: JQuery) => void,
    ) {
        function after(dialog: JQuery) {
            if (dialog.hasClass('open')) {
                dialogActions.close(dialog);
                return;
            }
    
            dialog.off('click').on('click', '.emoji-link', (e) => {
                e.preventDefault();
                const name = (e.currentTarget as HTMLAnchorElement).name;
    
                onClick(e, name, dialog);
            });
    
            //If Quill
            if($('[data-uuid="' + activeComposer + '"] .ql-container.ql-snow'))
                opener = (<HTMLScriptElement[]><any>document.querySelectorAll('button.ql-emoji-add-emoji'))[0];
    
    1. Update toggleForInsert as follows. Note the changes allowing emojis in title bar:
    export function toggleForInsert(textarea: HTMLTextAreaElement) {
        toggle($('[data-format="emoji-add-emoji"]').filter(':visible')[0], (e, name) => {
            var quill = $('[data-uuid="' + activeComposer + '"] .ql-container.ql-snow').data("quill");
            if(quill) {
                var range = quill.getSelection();
                if (range) {
                    if (range.length == 0) {
                        //User cursor is at index
                    } else {
                        //User has highlighted text - deleting and replacing with emoji
                        quill.deleteText(range.index, range.length);
                    }
                    quill.clipboard.dangerouslyPasteHTML(range.index, (<HTMLElement>(<HTMLInputElement>e.currentTarget).children[0]).outerHTML);
                    return;
                }
    
                //User cursor is not in editor (range is null) -> change focus to the title's textarea
                textarea = <HTMLTextAreaElement>(<any>(<HTMLScriptElement>document.querySelectorAll('.composer.resizable[data-uuid="' + activeComposer + '"] input.title.form-control')[0]));
                var text = <string>(<any>(<HTMLInputElement>e.currentTarget).children[0]).alt;
                var selectionStart = <number>(<any>(<HTMLScriptElement>document.querySelectorAll('.composer.resizable[data-uuid="' + activeComposer + '"] input.title.form-control')[0])).selectionStart;
                var selectionEnd   = <number>(<any>(<HTMLScriptElement>document.querySelectorAll('.composer.resizable[data-uuid="' + activeComposer + '"] input.title.form-control')[0])).selectionStart;
            } else {
                var text = `:${name}: `;
                var {selectionStart, selectionEnd} = textarea;
            }
            var end   = selectionEnd + text.length;
            var start = selectionStart === selectionEnd ? end : selectionStart;
    
            insertIntoTextarea(textarea, text);
            updateTextareaSelection(textarea, start, end);
            $(textarea).trigger('input');
        });
    }
    

    Note I am using dangerouslyPasteHTML that slightly changes the link/img html of the emoji element (for example, removes emoji classes). We can use the quill converter from quill-delta-to-html, perhaps it will make a slightly better job, or hardcode ourselves, that is, if these classes means anything...

    As for dangerouslyPaste, using the converter above will not make it any safer. We have to sanitize the delta in the server. Any attempts to sanitize in the client are futile, as someone can just take over the client code and push whatever he wants to the server...

    JJ.

  • Global Moderator Plugin & Theme Dev

    @JJSagan is there any specific reason you aren't implementing the same composer API which the redactor and default composers implement?

    Congrats on getting it to work, though.


  • Hi @PitaJ,

    Unfortunately Quill does not use textarea to store html code representing the edited text, but has its own JSON-like format. On the negative side, the default composer implementation will not work. On the positive side, this is a big plus for security, as someone can't just paste HTML and have that break the server. That does not mean we should not implement code-sanitize on the server side, as someone can post evil code that will be converted to html when other users view posts.

    Note that I edited my post above to include my final emoji-dialog code. It will now insert the emojis into the proper place, and also allow emojis in the post tile as an added bonus.

    Have a good weekend!
    JJ.

  • Global Moderator Plugin & Theme Dev

    @JJSagan does redactor have a textarea doing that? I didn't think it did. The purpose of the composer API is so that these manual implementations don't have to be done. If there are changes we need to make to the API for it to work with quils model, we can do that.


Suggested Topics

| | |

© 2014 – 2022 NodeBB, Inc. — Made in Canada.