Giter VIP home page Giter VIP logo

nodebb-plugin-composer-quill's Introduction

Quill composer for NodeBB

This plugin activates the WYSIWYG Quill composer for NodeBB. Please ensure that:

  • The markdown plugin is disabled (see note below, re: markdown)
  • Any other composers (i.e. nodebb-plugin-composer-default) are disabled
  • Warning This composer saves its data in a unique format that is only compatible with Quill. If you switch to Quill, any posts made with Quill cannot be migrated back to Markdown.

For developers

You may encounter a LESS build error when this module is not installed via npm:

error: [build] Encountered error during build step
Error: FileError: './quill/dist/quill.bubble.css' wasn't found. Tried - /some,/directories,/here
/quill.bubble.css,quill/dist/quill.bubble.css in /path/to/nodebb/node_modules/nodebb-plugin-composer-quill/static/less/quill.less on line 2, column 1:

This is due to npm/yarn's flattening of dependencies. Quill expects these css files to be at root level, so to get around this:

cd /path/to/nodebb/node_modules && ln -s nodebb-plugin-composer-quill/node_modules/quill .

Migration concerns

nodebb-plugin-composer-default/nodebb-plugin-markdown

If you used the default composer, chances are you also had the markdown plugin active. If that is the case, any posts made before the switch are still in markdown format, and are not saved into html (in a manner than quill can digest). A migrator tool has been bundled with v1.1 of the Quill Composer, which you can use to migrate posts in markdown and html into the Quill-compatible format.

nodebb-plugin-redactor

Your posts should automatically work with Quill. Redactor saves HTML into the database, and the Quill plugin is set up so it can digest that html and backport it to Quill's internal format if the post is edited. That said, when you edit a post originally made in Redactor, you'll see the html tags, which are now extraneous. You can remove them as part of your edit. Alternatively, you can use the bundled migrator to convert posts to the Quill-compatible format.

Contributors Welcome

This plugin is considered production ready. Please report any bugs to our issue tracker and we will take a look.

If you'd like to look at the documentation and add a feature, or take a look at the GitHub Issues and do something from there then please do. All pull requests lovingly reviewed.

Screenshots

Desktop

Desktop

Mobile

Mobile

nodebb-plugin-composer-quill's People

Contributors

barisusakli avatar dependabot[bot] avatar gasoved avatar julianlam avatar renovate-bot avatar renovate[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nodebb-plugin-composer-quill's Issues

Chat: Empty messages are sent to the server

Hi @julianlam

Apology for the number of issues filed recently ;)

Another one surfaced: Following the very first chat message, quill will insert '\n' at the end of each subsequent message, even if the text window is clear.

We can eliminate empty messages with the below code, inserted into public/src/chats/messages.js right after if(!msg.trim().length).

Note I could have compared msg with specific string, such as: {"ops":[{"insert":"\n"}]}. Unfortunately, it turns out that attributes are added as well, for example, in RTL the string will look like this: {"ops":[{"attributes":{"direction":"rtl","align":"right"},"insert":"\n"}]}. Since the plugin needs to support multiple configurations, parsing JSON is probably a better choice.

messages.sendMessage = function (roomId, inputEl) {
		var msg = inputEl.val();
		var mid = inputEl.attr('data-mid');

		if (!msg.trim().length) {
			return;
		}

                //For Quill
		try {
			let tmp_msg = JSON.parse(msg);
			if(tmp_msg.hasOwnProperty('ops') && (tmp_msg.ops.length === 1) && tmp_msg.ops[0].hasOwnProperty('insert') && (tmp_msg.ops[0].insert === '\n'))
				return;
		} catch(e) {
			//Regular string
		}

Migrator for markdown

Quill composer should contain a utility to convert posts written in Markdown to Quill formatting. Simple option to scan all posts and skip those with quillDelta should suffice.

Don't save quill deltas into post data content

There have been concerns raised about the usage of quill deltas and how it is saved in content in the post hash. If for any reason the plugin is disabled, stringified quill deltas will be shown, which in and of itself is not that bad, just kind of ugly.

On save, the delta should be saved into quillDelta, and an HTML fallback saved to content. On the way out, content (which should already be html) is read, so we can completely forgo the parsing hooks.

On edit raw content retrieval, we will send quillData instead of content.

Add ToolTips to Quill Menu

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

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]);
				if(isSpan)
					document.querySelector(composer_selector + ' .' + tool).setAttribute('title', tooltips[tool]);
				else
					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();
	}

});

Upload picture accept types

Hi @julianlam

Please consider adding accept to the file input:

  1. In formatting.js (composer) add picture: function() as follows:
var formattingDispatchTable = {
		picture: function () {
			var postContainer = this;
			postContainer.find('#pictures').click();
		},

		upload: function () {
			var postContainer = this;
			postContainer.find('#files').click();
		},
  1. In composer.tpl (that I copied from redactor ;) ) add input with id=pictures:
<form id="fileForm" method="post" enctype="multipart/form-data">
						<!--[if gte IE 9]><!-->
						<input type="file" id="files" name="files[]" multiple class="gte-ie9 hide"/>
						<input type="file" id="pictures" accept="image/*, video/*" name="files[]" multiple class="gte-ie9 hide"/>
						<!--<![endif]-->
						<!--[if lt IE 9]>
						<input type="file" id="files" name="files[]" class="lt-ie9 hide" value="Upload"/>
						<input type="file" id="pictures" accept="image/*, video/*" name="files[]" class="lt-ie9 hide" value="Upload"/>
						<![endif]-->
					</form>

A better way is to use the ACP defined mime types for files and pictures, but I have not been able to find such field in config.
Thanks!

Migrator for chat

Hi @julianlam ,
How are the chats being migrated. I have an issue in which some chat messages are showing quill format, and can't find the migration code.
thanks,
JJ.

Text direction and alignment are set incorrectly

Hi,
In quill-nbb.js init:
It seems that quill.setContents overrides the direction.
I believe direction should be included with the data.composerData.body so it will be set properly immediately after quill.setContents is called.
For now, (quick and dirty fix), lets move the direction setting after restoring body text (if contained in composerData), as follows:

// Restore text if contained in composerData
		if (data.composerData && data.composerData.body) {
			try {
				var unescaped = data.composerData.body.replace(/&quot;/g, '"');
				quill.setContents(JSON.parse(unescaped), 'api');
			} catch (e) {
				quill.setContents({ ops: [{ insert: data.composerData.body.toString()}]}, 'api');
			}
		}

		// Automatic RTL support
		quill.format('direction', textDirection);
		quill.format('align', textDirection === 'rtl' ? 'right' : 'left');

SocketPosts.getRawPost should return delta

This is a similar issue to #72 (comment)
socket.io/posts.js currently returns html data, while it should return delta when Quill is used.
I adjusted the file just to point where the issue is. Please decide how you want to go about telling the core that quill is being used, and then I'll revise the code to work with all other plugins.

SocketPosts.getRawPost = function (socket, pid, callback) {
        async.waterfall([
                function (next) {
                        privileges.posts.can('topics:read', pid, socket.uid, next);
                },
                function (canRead, next) {
                        if (!canRead) {
                                return next(new Error('[[error:no-privileges]]'));
                        }
                        //posts.getPostFields(pid, ['content', 'deleted'], next);
                        posts.getPostFields(pid, ['quillDelta', 'deleted'], next);
                },
                function (postData, next) {
                        if (postData.deleted) {
                                return next(new Error('[[error:no-post]]'));
                        }
                        //next(null, postData.content);
                        next(null, postData.quillDelta);
                },
        ], callback);
};

Chat: Icons for image upload and video links

To enable:

$(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>' +
'</div>';
var targetEl = $(html).insertBefore(components.get('chat/input'));

Its preferable to change the theme to snow.
The .less need revision to position the icons above the textbox.

[Enhancement] Slugify username in @mentions

Issue relates to latest version.

Lets assume users have the following userslugs
A Z1
A Z2
A Z3...
...
A Z1000

Lets assume someone is trying to mention @A Z800,
Since textcomplete has limited (10 users) selection box, and since the selection box will be removed if the user entered space, selecting A Z800 will not be possible.
Slugifing the username will allow the user to enter "A-Z80" and then complete using the selection box.

Chat length is incorrectly measured by server

At the client, the chat length is measured properly. The server however measures the length of the quill delta string. That causes even very short messages to be rejected due to size.

Sockets.push() should return quill data

Hi @julianlam
If we wish to edit a post, I believe it makes sense to return delta from the server, and not the html data. I have revised composer-default websockets.js to do the same. The changes just resolve the issue for quill, and will obviously break other plugins. I am unsure how you want to go about telling composer that quill is in use:

Sockets.push = function(socket, pid, callback) {
        var postData;
        async.waterfall([
                function (next) {
                        privileges.posts.can('topics:read', pid, socket.uid, next);
                },
                function (canRead, next) {
                        if (!canRead) {
                                return next(new Error('[[error:no-privileges]]'));
                        }
                        posts.getPostFields(pid, ['quillDelta', 'tid', 'uid', 'handle'], next);
                },
                function (_postData, next) {
                        postData = _postData;
                        if (!postData && !postData.quillDelta) {
                                return next(new Error('[[error:invalid-pid]]'));
                        }
                        async.parallel({
                                topic: function(next) {
                                        topics.getTopicDataByPid(pid, next);
                                },
                                tags: function(next) {
                                        topics.getTopicTags(postData.tid, next);
                                },
                                isMain: function(next) {
                                        posts.isMain(pid, next);
                                }
                        }, next);
                },
                function (results, next) {
                        if (!results.topic) {
                                return next(new Error('[[error:no-topic]]'));
                        }
                        plugins.fireHook('filter:composer.push', {
                                pid: pid,
                                uid: postData.uid,
                                handle: parseInt(meta.config.allowGuestHandles, 10) ? postData.handle : undefined,
                                body: postData.quillDelta,
                                title: results.topic.title,
                                thumb: results.topic.thumb,
                                tags: results.tags,
                                isMain: results.isMain
                        }, next);
                },
        ], callback);
};

Resize image in client

Hi @julianlam ,
I believe we should resize uploaded images in the client, saving server CPU time and bandwidth.
To accomplish, lets call upload.show() instead of sending the file directly to /api/...
In upload.show, we may implement resizing.
Hope that's acceptable.
JJ,

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: undefined. Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.

Deleting the entire content of the editor switches Direction

Solution that works (for now):

quill.on('text-change', function () {
			if (isEmpty(quill)) {
				console.log('empty!!');
				//quill.deleteText(0, quill.getLength());
				//quill.setText('&#8203;');
				textareaEl.val('');
				return;
			}
			textareaEl.val(JSON.stringify(quill.getContents()));
			textareaEl.trigger('change');
		});

The new message button no longer works.

Good evening,
I have a problem on my forum with the module, when I click on the "new message"/ "reply" button, nothing happens. As if the button didn't exist.

Do you know where this problem comes from?

I downloaded the MASTER archive that I decompressed and sent to my server, then installed the module, taking care to disable all other compositers and BBcodes modules. I have the latest version of NodeBBB (1.12.1)

Here is a short video that shows this:

http://concepts.esenjin.xyz/cyla/v2/file/D57D3C.mp4

Thanks !

Emojis

Hi @julianlam, @pitaj,

Emoji's dialog box will be displayed only when this fix is done on the emoji plugin:
NodeBB/nodebb-plugin-emoji#29 (comment)

However, once the fix is done, and an emoji is selected from the dialog box, we hit the following issue:
Uncaught TypeError: t.context.getSelection is not a function

In this function:
quill-nbb.js

$(window).on('action:composer.insertIntoTextarea', function (evt, data) {
		var selection = data.context.getSelection(true);
		data.context.insertText(selection.index, data.value);
		data.preventDefault = true;
	});

Looks like data.context has the following functions declared:
getBlockData: ƒ (e,t,n)
insertIntoTextarea: ƒ (t,n)
updateTextareaSelection: ƒ (e,t,n)
wrapSelectionInTextareaWith:

I also looked at emoji plugin and found out that it does not exports getSelection, perhaps it should?

As for data.context.insertText(selection.index, data.value);
perhaps you wanted to call data.context.insertIntoTextarea instead?

Support quick-reply with expand button

It would be nice if quill could be used for quick reply, allowing the user to expand the window to full page if needed, or collapse it back to the original quick reply spot.

How to add a url?

Hello,

Sorry, I'll be back with a new problem. There is no button to add a hypertext link, whereas we can see that there is a tool for this, when we copy/paste a link.

ql-tooltip

I thought it might have been an option to check in the administration panel, but it is impossible to access the page /admin/plugins/composer-quill

ql-admin

I wish you can help me.

Restore Text, Remove BlockQuotes from last line

The below code will add a "clean" line following the quoted text.
It will remove block-quotes and reset direction and alignment on the last line.

// Restore text if contained in composerData
		if (data.composerData && data.composerData.body) {
			try {
				var unescaped = data.composerData.body.replace(/&quot;/g, '"');
				let delta = JSON.parse(unescaped);
				//Remove blockquotes:
				delta.ops.push({
					insert: '\n',
					attributes: {
						direction: textDirection,
						align: textDirection === 'rtl' ? 'right' : 'left'
					}
				});
				quill.setContents(delta, 'api');
			} catch (e) {
				quill.setContents({ ops: [{
						insert: data.composerData.body.toString(),
						attributes: {
							direction: textDirection,
							align: textDirection === 'rtl' ? 'right' : 'left'
						}
					}]}, 'api');
			}
		} else {
			// Automatic RTL support
			quill.format('direction', textDirection);
			quill.format('align', textDirection === 'rtl' ? 'right' : 'left');
		}

Emoji or Mention insertion bumps the cursor index too far

Steps to reproduce: add mention or emoji.
The cursor should be placed right after the inserted mention/emoji, but unfortunately it is placed far from it. I suspect it has to do with Quill not counting characters in the same manner composer does.

[Enhancement] Use quill-delta-to-html

Seems that this package is better in a sense that the development is active.
Furthermore, output may be customized.
Works better for rtl, although not perfect...

how to convert existing posts?

Warning This composer saves its data in a unique format that is only compatible with Quill. You'll have to use the bundled importer/exporter to convert existing posts if necessary.

can u tell me where is the importer/exporter tool? I can't find it. thank you.

Support post quote

Hi @julianlam

Pertaining plugin.handleRawPost

It seems that SocketPosts.getRawPost adds a prefix "postData" to the post data.
To that end, please consider the below changes:

plugin.handleRawPost = async (data) => {
	if (data.hasOwnProperty('pid'))
		data.content = await posts.getPostField(data.pid, 'quillDelta');
	else
		data.postData.content = await posts.getPostField(data.postData.pid, 'quillDelta');

	return data;
};

Upload and Picture handlers should be bound to the current editor

In nbb-quill.js:

This line should change from:

toolbarOptions.handlers[name] = toolbarHandlers[name].bind($('.formatting-bar'));

To:

toolbarOptions.handlers[name] = toolbarHandlers[name].bind(textareaEl.parents('.composer-container').find('.formatting-bar'));

Clicking the emoji icon does not work on first composer dialog

Hi @julianlam ,

The emoji handler is not ready at the time init is called. That makes sense since init also initializes the emoji plugin with the new composer uuid. On the second call to init, obviously this will not be an issue.

Since AFAIK there is no other hook that fires when emoji is ready, how about doing the following (right before init callback):

if(typeof toolbarHandlers['emoji-add-emoji'] === "undefined") {
			var timer = setTimeout(function () {
				toolbarHandlers = formatting.getDispatchTable();
				if (toolbarHandlers['emoji-add-emoji']) {
					var toolbar = quill.getModule('toolbar');
					var selection = quill.getSelection(true);
					toolbar.addHandler('emoji-add-emoji', function () {
						toolbarHandlers['emoji-add-emoji'].apply(quill, [textareaEl, selection.index, selection.index + selection.length]);
					});
					clearTimeout(timer);
				}
			}, 1000);
		}

quill path problem

version:
nodebb 1.12
nodebb-plugin-composer-quill 1.1.1

problem:
File not found! nodebb-plugin-composer-quill/node_modules/quill/dist/quill.bubble.css

I found that quill is not installed in the local path, so it is not found. So is it caused by npm version? or do I miss some other config?

Adding Emoji from GUI will not show emoji unless enter is pressed

First, the previous code that got the selection from composer will not work with quill (namely, data.context.getSelection(true) does not exist).

To that end, we extract quill from textarea. then insert the emoji at the proper index.

It is odd however, that insertText does not show the emoji (it will show ":face " for example), unless the user pushes CR after the insertion.

This was resolved by replacing insertText with dangerouslyPasteHTML.
While the solution works, it may indicate an issue.

$(window).on('action:composer.insertIntoTextarea', function (evt, data) {
		//var selection = data.context.getSelection(true);
		//data.context.insertText(selection.index, data.value);
		let quill = $(data.textarea).parent().find('.ql-container').data('quill');
		if(quill) {
			let range = quill.getSelection();
			//const text = `:${name}: `;
			const text = `${data.value} `;
			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.insertText(range.index, text);
				quill.clipboard.dangerouslyPasteHTML(range.index, text);
                                //Focus quill:
				quill.setSelection(range.index + 1, 0);
			}
		}
		data.preventDefault = true;
	});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.