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

Technical Support
  • #1

    Hi Devs,

    Given the licensing issues with Redactor, I started transitioning to quill, using @julian 's latest nodebb-plugin-composer-quill.

    Overall the plugin is in good shape, and quill is just great and stable tool.

    If any of you wish to make a switch, it might be helpful to note the following:

    1. The plugin uses a simple JSON format, instead of the good old string format we were used to. That means special care must be taken both at the server and at client. Be careful! if you have JS that are used on both the client and server, make sure to return string in each function - use JSON.stringify() where needed.

    2. You may access quill from your plugin - the quill plugin saves a ptr as follows:'quill', quill);
      so in your plugin you may:
      var quill = $('[data-uuid="' + + '"] .ql-container.ql-snow').data("quill");

    3. I have not added tooltips, but I understand it should be done as follows:

    4. chat: I like having icons for image upload and video links. To enable that change the line to:
      $(window).on('action:chat.loaded', function (e, containerEl) {
      // Create div element for composer
      //var targetEl = $('<div></div>').insertBefore(components.get('chat/input'));
      var html =
      '<div></div>' +
      '<div class="btn-toolbar formatting-bar">' +
      '<form id="fileForm" method="post" enctype="multipart/form-data">' +
      '<input type="file" id="files" name="files[]" multiple="" class="gte-ie9 hide">' +
      '</form>' +
      var targetEl = $(html).insertBefore(components.get('chat/input'));

      Also change the theme to snow. You also need to change the .less a bit to position the icons above the textbox.

      *** Note, with the above targetEl changes, I am getting the file upload dialog box, but after selecting a file I am not getting the upload event - still working on this one... @julian if you have some time please help.

    5. on chat send, rework the code to return the writing direction to defaults :
      $(window).on('action:chat.sent', function (e, data) {
      ...// add at the end:
      var textDirection = $('html').attr('data-dir');
      quill.format('direction', textDirection);
      quill.format('align', textDirection === 'rtl' ? 'right' : 'left');

    6. See $(window).on('action:composer.uploadUpdate', function (e, data) and change the path in:
      if (data.text.startsWith('/'))
      to where your upload files are located.

    7. In the same function there's some code that adds random string to the filename to avoid collision. If you are already handling this, make sure to remove it.

    8. in composer.tpl, it may make sense to move the tags line below the topic - currently its in the bottom.

    9. Haven't looked at sanitizing quill at the server, but found a lead here:

    10. The chat teaser (when you load the chat menu) displays the JSON quill format, that is, the data needs to be filtered to extract the text. That requires changes on the nodebb core files, and I can't make these...

    That's where I am at. I'll update as I make more progress.

    Overall quill is really good editor, it may be worth while for you to dive in.
    Happy hunting,

  • #2

    Pertaining (1) above, I suggest augmenting parseRaw as below. It will help catch and auto-fix conversion issues. Do note that chat (html) messages are flagged as wrong format.

    plugin.parseRaw = function (raw, callback) { 
            if((raw != null) && (raw.hasOwnProperty("ops"))) {
    		raw = JSON.stringify(raw);
    		winston.error('Quill JSON detected - ', raw);
    	try {
    		var unescaped = raw.replace(/&quot;/g, '"');
    		var content = JSON.parse(unescaped);
    		var converter = new QuillDeltaToHtmlConverter(content.ops, {});
    		raw = converter.convert();
    	} catch (e) {
    		// Do nothing
    		winston.warn('[composer-quill] Input not in expected format, skipping. - ', raw);
    	callback(null, raw);
  • #3

    Hi @julian

    Pertaining (4), I was able to get an image/file uploaded in chat, using the following hack:

    1. Require composer/uploads in quill-nbb.js and make the following changes to uploads.js:

    Wherever there postContainer is initialized:

    	var postContainer = $('.composer[data-uuid="' + post_uuid + '"]').length ? $('.composer[data-uuid="' + post_uuid + '"]') : $('.chat-modal[data-uuid="' + post_uuid + '"]');
    1. Augment quill-nbb.js as follows:
             $(window).on('action:chat.loaded', function (e, containerEl) {
    		// Create div element for composer
    		//var targetEl = $('<div></div>').insertBefore(components.get('chat/input'));
    		var html =
    			'<div></div>' +
    			'<div class="btn-toolbar formatting-bar">' +
    				'<form id="fileForm" method="post" enctype="multipart/form-data">' +
    					'<input type="file" id="files" name="files[]" multiple="" class="gte-ie9 hide">' +
    				'</form>' +
    		var targetEl = $(html).insertBefore(components.get('chat/input'));
    		var onInit = function () {
    			setTimeout(uploads.initialize, 2000, containerEl.dataset.uuid);

    The setTimeout is necessary, since at the time the chat.loaded event is called, the modal is still hidden.

    Now that the file was uploaded, I need to deal with displaying it on the chat window. Currently I am getting JSON quill 🙂

    Will continue to explore.

  • #4

    Fixing the below in quill-nbb.js gets the uploaded image properly registered and displayed in the chat window:

    $(window).on('action:composer.uploadUpdate', function (e, data) {
    		//var quill = components.get('composer').filter('[data-uuid="' + data.post_uuid + '"]').find('.ql-container').data('quill');
    		if($('.composer[data-uuid="' + data.post_uuid + '"]').length)
    			var quill = components.get('composer').filter('[data-uuid="' + data.post_uuid + '"]').find('.ql-container').data('quill');
    			var quill = $('.chat-modal[data-uuid="' + data.post_uuid + '"] .ql-container.ql-snow').data("quill");

    I'll run some more tests - and then move on to video.

  • #5

    Hi @julien ,

    As for fixing the teaser (issue #10):

    There are two options,

    1. Preferable: Fixing at the server - file src/messaging.js

    Messaging.getTeaser = function (uid, roomId, callback) {

    in line 235 add:

        if((teaser.content != null) && (teaser.content.hasOwnProperty("ops"))) {
    	teaser.content = JSON.stringify(teaser.content);
    try {
    	var unescaped = teaser.content.replace(/&quot;/g, '"');
    	var content = JSON.parse(unescaped);
    	var converter = new QuillDeltaToHtmlConverter(content.ops, {});
    	teaser.content = converter.convert();
    } catch (e) {
    	// Do nothing
    1. In the client: ./src/modules/chat.js:123

    module.loadChatsDropdown = function (chatsListEl) {
    		socket.emit('modules.chats.getRecentChats', {
    			uid: app.user.uid,
    			after: 0,
    		}, function (err, data) {
    			if (err) {
    				return app.alertError(err.message);
    			var rooms = data.rooms.filter(function (room) {
                                   if((room.teaser != null) && (room.teaser.hasOwnProperty("ops"))) {
                            		room.teaser = JSON.stringify(room.teaser);
         	                try {
    		              var unescaped = room.teaser.replace(/&quot;/g, '"');
    		              var content = JSON.parse(unescaped);
    		              var converter = new QuillDeltaToHtmlConverter(content.ops, {});
    		              room.teaser = converter.convert();
    	                  } catch (e) {
    		             // Do nothing
    				return room.teaser;

    The above code was untested! I don't know how to override js files and keep my changes after pull 😉

    I really hope you'll consider positively making the changes in uploads.js and in messaging.js. They are very short and can really help thanks!

  • #6

    Pertaining tooltips (#3):

    In action:composer:loaded add the following function call:

    [Code was edited, so as to catch all buttons - some do not have <button tags]:

    Call this function by:
    		toolTip('.composer[data-uuid="' + data.post_uuid + '"]');
    function toolTip (composer_selector) {
    		let tooltips = {
    			'bold': 'Help I am BOLD',
                            //your icons - perhaps add translate here
    		let showTooltip = (el, isSpan) => {
    			let tool = (isSpan ? el.className.replace('span', '') : el.className.replace('ql-', '')).replace(/\s+/g, '.');
    			if (tooltips[tool]) {
    				console.log('toolTip: ' + tooltips[tool]);
    					document.querySelector(composer_selector + ' .' + tool).setAttribute('title', tooltips[tool]);
    					document.querySelector(composer_selector + ' .ql-' + tool).setAttribute('title', tooltips[tool]);
    		let toolbarElement = document.querySelector(composer_selector + ' .ql-toolbar');
    		if (toolbarElement) {
    			let matches = toolbarElement.querySelectorAll('button');
    			for (let el of matches) {
    				showTooltip(el, false);
    			toolbarElement.querySelectorAll(':scope > span').forEach(function(element) {
    				matches = element.querySelectorAll(':scope > span');
    				for (let el of matches) {
    					showTooltip(el, true);
    		//Enable tooltip
    		$(composer_selector + ' [data-toggle="tooltip"]').tooltip();

    Till now I have documented all the changes I can take care of without changing the core code.

    @julian Kindly consider changing composer/uploads.js to allow uploads from chat, and the server code to return the teasers properly - The changes are very small and are documented in the previous message.

    Devs, quill can give composer-redactor a good beating.
    Take a look, I bet you'll love it.

  • GNU/Linux

    Oh yeah -- Redactor is old and not supported anymore because they don't have an open source license. It is their call not to have one, so I must respect that.

    Quill is still in development, so I would love to take your changes into account. It is not quite production ready yet.... for one, I still have to build conversion tools to and from quill in case you switch composers.

  • #8

    @julian ,

    Thank you 🙂 kindly note that I am novice programmer and my code may be written better / more efficiently.

    Did emoji work for you? cause I see that in the screenshots you have emoji icon displayed.
    When I enable nodebb-plugin-emoji latest version (2.2.6) and push the icon I get:

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

    My guess is that emoji is trying to feed the editor but does not know how (due to the new format). I suggest to deal with that in quill plugin. Maybe @PitaJ may have some comments about that.

    Another option is to install quill-emoji I'll try that and report back.

    Lastly, I believe the search plugin may need modification. Search may be enabled as follows:

    • Getting the data to the server to remove the quill format (performance hit), or

    • Double saving post data (once in quill format, and another just with text, removing quill json and html tags). That will consume more space but search can be done on the database with no performance hit.

    • Searching as-is remembering that some words (like "insert") are reserved.


  • #9

    Hi @julian ,

    Just completed integrating quill-emoji lib. (

    First I should say that the icons will only display inside the editor, so you can't display them afterwards (without changes to the core nodebb files), nor they may be used outside the scope of the editor, for example, in a topic header. To that end, I suggest getting the standard emoji composer library to work.

    if anyone wants to experiment, allow me to save you some time:

    1. Update your dependencies in package.json:
    "quill-emoji": "^0.1.2",
    1. Update plugin.json:
             "css": [
            "modules": {
    		"quill.js": "./node_modules/quill/dist/quill.js",
    		"quill-emoji.js": "./node_modules/quill-emoji/dist/quill-emoji.js"
    1. update quill-nbb.js packages:
    define('quill-nbb', [
    ], function (Quill, QuillEmoji, ...
    1. update quill-nbb.js quill icons:
    var toolbarOptions = {
    				container: [
    					[ your icons],
    				handlers: {
    					'emoji': function () {}
    1. Update init():
    		var quill = new Quill(targetEl.get(0), {
    			theme: data.theme || 'snow',
    			modules: {
    				toolbar: toolbarOptions,
    				"emoji-toolbar": true,
    				"emoji-shortname": true,
    				"emoji-textarea": true

    That's it, you should be all set.
    But as I said, this solution is inferior, a better solution is using nodebb-plugin-emoji.
    @PitaJ , see my previous message with the error code. Will appreciate some help thanks!


  • Global Moderator Plugin & Theme Dev

    @JJSagan could you get a screenshot of where the error occurs in the code? You might need to run in dev mode if the sourcemaps don't work.

  • #11
    This post is deleted!
  • #12

    The issue occurs in emoji.js at line 100:

    0_1538246193207_Untitled.png = fuzzySearch;
                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);  //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! this line
                if (callback) {
                    setTimeout(callback, 0);
  • #13
    This post is deleted!
  • #14

    Line 100 toggleForInsert(textarea) articulates the issue I discussed earlier.

    Unfortunately with quill, one cannot write into the text area. The quill format is a bit different.

    Maybe you can return a link to quill-nbb.js so we can take care of inserting the data property there.

    Another option is for you to check if quill is in use, and then you may:
    quill.insertText(ptr, markup_txt, {
    "align": "??",
    "color": "??",
    where ptr is the point to which the icon should be inserted. I assume you are not saving a bitmap but a link. To that end please take a look at quill-nbb.js specifically at image upload. Once a link is uploaded quill can display it.

  • GNU/Linux

    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.

  • #16

    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.

  • #17

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

  • #18

    Hi @Kosiak
    You'll have to clone it from here:
    Its not PnP, yet... 🙂 so get yourself ready for few code changes - no biggie, just follow the above.
    Good luck!

  • #19

    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!

  • #20

    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 😉

Suggested Topics

| | | |