Quill.js: registering PlainClipboard causes initial editor content to lose all formats - rich-text-editor

The PlainClipboard shown in the doc is very useful when we want Quill to clear all formats when content is pasted (e.g., copied from webpages). But if we register this module as 'modules/clipboard', any formats in the initial editor container will be wiped out as well:
<div id="editor-container">
<ol><li>1</li><li>2</li><li>3</li></ol>
</div>
because the initial content is also processed by the convert() function when the editor is initialized. To workaround this, I used a flag to indicate whether it's editor initialization or normal user pasting:
var quill_initializing = true;
class PlainClipboard extends Clipboard {
convert(html = null) {
if (quill_initializing) {
quill_initializing = false;
return super.convert(html);
} else {
if (typeof html === 'string') {
this.container.innerHTML = html;
}
let text = this.container.innerText;
this.container.innerHTML = '';
return new Delta().insert(text);
}
}
}
This works for Quill 1.3.1 but after I updated to Quill 1.3.2, this approach stops working.
Is there a way to fix this?

Related

Conditional class binding using computed prop Vue 2

I wanted to give some form visual validation cues, so I tried using class binding to do just that. If I use the ternary inline, it doesn't really meet my requirements of what should happen, but when I tried using computed prop, it made all the other components disappear.
If i tried doing it like this: v-bind:class="[(!validation.hasError('form.fullName'))?'has-success':'has-danger noValid']"
It just triggers the animation and the classes once and they stays there. I want to trigger the noValid animation everytime the user clicks my submit button if there's errors in validation.
I'm using simple-vue-validator btw.
Here's the godforsaken component, I use vue now-ui-kit template from Creative Tim as a base and customize my way from there. This is their Form Group Input component, docs here
<fg-input
class="input-lg"
:label="validation.firstError('form.fullName')"
placeholder="Full Name..."
v-model="form.fullName"
addon-left-icon="now-ui-icons users_circle-08"
v-bind:class="{ visualValidation }"
></fg-input>
Button Component: bootstrap-vue, cause their customized button doesn't really serve my purpose
<b-button type="submit" block pill variant="info" #click="submit">Submit Form</b-button>
My computation: noValid is the shaking animation class, has-success and has-danger just changes their appearances.
computed: {
visualValidation: function() {
const successClass = "has-success";
const errorBoi = "has-danger";
const shakeBoi = "noValid";
if (validation.firstError("form.fullName")) {
return errorBoi + " " + shakeBoi;
} else if (!validation.hasError("form.fullName")) {
return successClass;
}
}
}
I thought the variables that i returned would be binded as classes to the form.fullName Model but it's not doing anything, better yet, it made all my other components not rendering.
What should i do here? i'm just starting to learn Vue.js so i don't really understand what's going on here.
Edit from here:
I rewrote the logic to my class binding, and just use method to remove the class on click the components.
here is the updated component:
<fg-input
class="input-lg"
:label="validation.firstError('form.email')"
placeholder="Email Here..."
v-model="form.email"
addon-left-icon="now-ui-icons ui-1_email-85"
v-bind:class=" {'has-success' : isSuccEmail, 'has-danger' : isFailEmail, 'noValid': validNoEmail} "
#click="removeShake()"
></fg-input>
data:
isSuccEmail: false,
isFailEmail: false,
validNoEmail: false
method:
removeShake: function() {
let vm = this;
vm.validNoName = false;
vm.validNoEmail = false;
console.log(vm.validNoEmail);
console.log(vm.validNoName);
},
However, there's currently a bug, where the validator don't validate separately. It gave the class has-success to email even though it was full-name that was successful.
valEmail: function() {
let vm = this;
vm.$validate("form.email").then(function(success) {
if (success) {
vm.isFailEmail = false;
vm.isSuccEmail = true;
} else if (!success) {
vm.isSuccEmail = false;
vm.isFailEmail = true;
vm.validNoEmail = true;
} else {
alert("Fatal Error");
}
});
},
valName: function() {
let vm = this;
vm.$validate("form.fullName").then(function(success) {
if (success) {
vm.isFailName = false;
vm.isSuccName = true;
} else if (!success) {
vm.isSuccName = false;
vm.isFailName = true;
vm.validNoName = true;
console.log(vm);
} else {
alert("Fatal Error");
}
});
}
The $validate is a function of simple-vue-validator, the docs.
Submit function is just calling those two functions above.
I think this because of the promise call, is there a way to call the $validate() without promise?
There are a few problems here.
Firstly, while templates don't require you to use this. you still need to use it within your JS code. You should be seeing an error in the console, or maybe even at compile time depending on how you have linting configured.
if (validation.firstError("form.fullName")) {
What is validation? I assume that should be this.validation. Likewise a couple of lines further down.
Your next problem is here:
v-bind:class="{ visualValidation }"
The braces here are creating an object literal, so it's equivalent to this:
v-bind:class="{ visualValidation: visualValidation }"
This will be conditionally adding the string 'visualValidation' as a class , which isn't what you want. Get rid of the braces:
v-bind:class="visualValidation"

Ckeditor issue calling setData during onchange event

I am trying to create a plugin that works similar to the tagging feature here on Stack Overflow. The plugin adds an onchange event to the editor and than checks the data to see if the user entered a tag and replaces any tags found with a div.
CKEDITOR.plugins.add('tagit', {
icons: '',
init: function (editor) {
var tags = ['MyTag'],
tokens = [];
editor.on('change', function (event) {
var tokenUpdated = false;
tokens = tokenize(event.editor.getData());
for (var tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) {
var token = String(tokens[tokenIndex]);
if (!token.match(/tagit/gmi) && tags.some(function (tag) { return token.indexOf(tag) >= 0; })) {
tokens[tokenIndex] = '<div class="tagit">' + tokens[tokenIndex] + '</div>';
tokenUpdated = true;
}
}
if (tokenUpdated) {
event.editor.setData(tokens.join(''));
}
});
var tokenize = function (data) {
var match = '(<div class="tagit">.*?<\/div>)';
for (var i = 0; i < tags.length; i++) {
match += '|(' + tags[i] + ')';
}
var re = new RegExp(match, "gmi");
return data.split(re);
}
}
});
The problem is when I call setData the change event is fired again and event.editor.getData() returns the html before I called setData. Is the change event fired before the data has actually been set? There's an option internal that I tried setting to true but than the data doesn't appear to be updated.
You are changing editors content so it's natural that change event will be called with editor.setData function. TBO I think that your implementation has a much important problem than circular call - you are comparing HTML content by regex. It's bad practice and you will encounter more problems during this implementation.
This feature is not obvious and requires working with document selection, not simply querying its content (also for performance reasons).
But I have a good information. With CKEditor 4.10 we are shipping new plugins which can easily be used to create feature you are talking about - especially textmatch and textwatcher. Mentioned plugins will be shipped alongside with autocomplete and mentions plugins. You can read more about our progress on GH:
Mentions: https://github.com/ckeditor/ckeditor-dev/issues/1703
Autocomplete: https://github.com/ckeditor/ckeditor-dev/issues/1751
4.10 release is set on 26 June but it could change, check GH milestones for updates.
After release, I can provide some example implementation for your feature - but I'm sure that with new plugins it will be easy as pie.

Rich Text Editor (WYSIWYG) in CRM 2013

Sometimes it is useful to have the HTML editor in CRM interface. It is possible to implement the editor directly to CRM 2013. As editor we will use ckeditor which allows to use it without installation on the server.
Identify the field where you would like to use the rich text editor.
Create html-webresource which will define ckeditor. Go to Settings-Customizations-Customize the System-Web Resources.
In html editor of web resource, select the Source tab and insert the following code:
<html>
<head>
<title></title>
<script src="//cdn.ckeditor.com/4.4.7/standard/ckeditor.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
function getTextFieldName()
{
var vals = new Array();
if (location.search != "")
{
vals = location.search.substr(1).split("&");
for (var i in vals)
{
vals[i] = vals[i].replace(/\+/g, " ").split("=");
}
//look for the parameter named 'data'
for (var i in vals)
{
if (vals[i][0].toLowerCase() == "data")
{
var datavalue = vals[i][2];
var vals2 = new Array();
var textFieldName = "";
vals2 = decodeURIComponent(datavalue).split("&");
for (var i in vals2)
{
var queryParam = vals2[i].replace(/\+/g, " ").split("=");
if (queryParam[0] != null && queryParam[0].toLowerCase() == "datafieldname")
{
textFieldName = queryParam[1];
}
}
if (textFieldName == "")
{
alert('No "dataFieldName" parameter has been passed to Rich Text Box Editor.');
return null;
}
else
{
return textFieldName;
}
}
else
{
alert('No data parameter has been passed to Rich Text Box Editor.');
}
}
}
else
{
alert('No data parameter has been passed to Rich Text Box Editor.');
}
return null;
}
CKEDITOR.timestamp = null;
​// Maximize the editor window, i.e. it will be stretched to target field
CKEDITOR.on('instanceReady',
function( evt )
{
var editor = evt.editor;
editor.execCommand('maximize');
});
var Xrm;
$(document).ready(function ()
{
​ // Get the target field name from query string
var fieldName = getTextFieldName();
var Xrm = parent.Xrm;
var data = Xrm.Page.getAttribute(fieldName).getValue();
document.getElementById('editor1').value = data;
/*
// Uncomment only if you would like to update original field on lost focus instead of property change in editor
//Update textbox on lost focus
CKEDITOR.instances.editor1.on('blur', function ()
{
var value = CKEDITOR.instances.editor1.getData();
Xrm.Page.getAttribute(fieldName).setValue(value);
});
*/
// Update textbox on change in editor
CKEDITOR.instances.editor1.on('change', function ()
{
var value = CKEDITOR.instances.editor1.getData();
Xrm.Page.getAttribute(fieldName).setValue(value);
});
// Following settings define that the editor allows whole HTML content without removing tags like head, style etc.
CKEDITOR.config.allowedContent = true;
CKEDITOR.config.fullPage = true;
});
</script>
<meta>
</head>
<body style="word-wrap: break-word;">
<textarea class="ckeditor" cols="80" id="editor1" name="editor1" rows="10"></textarea>
</body>
</html>
Note:
As you can see, there are a few important sections
a) The following code loads the ckeditor and jquery from web so that they don't have to be installed on server.
<script src="//cdn.ckeditor.com/4.4.7/standard/ckeditor.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
b) Function getTextFieldName() which gets the name of target field where should be rich text editor placed. This information is obtained from query string. This will allow to use this web resource on multiple forms.
c) Initialization of ckeditor itself - setting the target field and properties of ckeditor. Also binding the editor with predefined textarea on the page.
Open the form designer on the form where you would like to use ​WYSIWYG editor. Create a text field with sufficient length (e.g. 100 000 chars) which will hold the html source code.
Insert the iframe on the form. As a webresource use the resource created in previous steps. Also define Custom Parameter(data) where you should define the name of the text field defined in step 4. In our situation we created new_bodyhtml text field so the parameter holds this value. This value is returned by the getTextFieldName() of the web resource.
Do not forget to save and publish all changes in CRM customization otherwise added webresources and updated form are not available.
That's all, here is example how it looks like:
Yes, you can use CKEditor, but when I implemented the CKEditor on a form, it turns out it is quite limited in the functionality in provides. Also, the HTML it generates leaves much to be desired. So, I tried a similar idea to Pavel's but using a backing field to store the actual HTML using TinyMCE. You can find the code here:
Javascript
HTML
Documentation
I have package my solution as a managed and unmanaged CRM solution that can be imported and utilised on any form. Moreover, it works on both CRM 2013 and CRM 2015

In jQuery, how can I create a custom event with a default?

The Event object in jQuery has this helpful preventDefault() method that prevents the default behaviour, obviously.
This is usually used to prevent click events from performing the browser default behaviour.
It seems like it would also be useful for custom events as well.
The task I'd like to achieve with this behaviour is a separate concern but I will explain it as an example for the behaviour I'm looking for:
I have a simple plugin that creates a popup out of a div. I found it on the internet.
$(selector).pop();
I have hacked it to close when you click on anything but a child of the popup, and to prevent default click behaviour on the clicked element.
function closeInactivePop() {
var foundAny = false;
jQ.each(function (i) {
var $this = $(this);
if ($this.hasClass('active') && ! $this.data('activePop')) {
$this.removeClass('active');
foundAny = true;
}
});
return foundAny;
}
$('body').click(function(){
// If we closed any, cancel the propagation. Otherwise let it be.
if (closeInactivePop()) {
$(document).trigger('jQuery.pop.menuClosed');
return false;
}
});
(Now that I paste it I realise I could have done this a bit better, but that notwithstanding).
Now I have added a new plugin that draws a colour picker inside the popup. Except the DOM that this colour picker creates is not inside the popup; it is only inside it visually. The DOM structure is separate.
In the aforementioned hack I would prefer to in fact fire another event, one whose default behaviour is to close the popup.
function closeInactivePop() {
var foundAny = false;
jQ.each(function (i) {
var $this = $(this);
if ($this.hasClass('active') && ! $this.data('activePop')) {
$(document).trigger('jQuery.pop.menuClosed');
$this.removeClass('active');
foundAny = true;
}
});
return foundAny;
}
$('*').click(function(e) {
var $this = $(this);
// This bit is pseudocode, where the Function is the default behaviour
// for this event.
// It is helpful that $this is actually the clicked element and not the body.
$this.trigger('jQuery.pop.menuBeforeClose', function() {
// if we run default behaviour, try to close the popup, or re-trigger the click.
if (!closeInactivePop()) {
$this.trigger(e);
}
});
});
Then I could later do
$('#colour-picker').bind('jQuery.pop.menuBeforeClose', function(e) {
e.preventDefault();
});
And this would prevent the closeInactivePopup default behaviour running when the target of the original click event was the colour picker or something inside it.
Can I do this somehow, even hackily?
I doubt that there is a native way to do that. However, you can either use "triggerHandler()" instead of "trigger()", which provides the ability to return values from the event handlers. Another relatively simple solution is to pass a custom "event" object that can be used to cancel the planned action:
function Action() {
var status = true;
this.cancel = function() { status = false; };
this.status = function() { return status; };
}
$('button').click(function() {
var action = new Action();
$(this).trigger('foo', [action]);
if (action.status()) {
// ... perform default action
}
});​
In the event handler:
$('*').bind('foo', function(event, action) {
action.cancel();
});

Tinymce is (sometimes) undefined

I'm using Tinymce (with jQuery) in a project I'm working at; we use a rich text editor for users to input information; however, sometimes when loading the page Firefox and Chrome will detect a 'tinymce is not defined' error (sometimes at different lines of the code), while other times the page will load just fine. What's weird is that it works perfectly with IE.
Here's a bit of the code I'm using:
view.find('textarea.rich-text').each(function () {
$(this).tinymce( /* ...rules... */);
});
And later on
_variable.find("#summary").tinymce().setContent(content);
This line is where the error (sometimes) gets caught. It seems to me that the problem is a loading issue, even though the tinyMCE plugin is initialized about 5000 lines prior this line.
Update: For now I have managed to 'solve' the problem with a setTimeout, but this seems like a really ugly way to do it.
A few points:
You don't mention whether or not the TinyMCE initialization is done within a jQuery ready event function. It should be of course.
You don't need the each loop. You can just say:
$('textarea.rich-text').tinymce({
script_url : '../js/tinymce/jscripts/tiny_mce/tiny_mce.js',
theme : "advanced",
...
});
You don't need the call to find since you are just selecting by id. Just do:
$("#summary").tinymce().setContent(content);
Your real issue is probably that tinymce has not finished initializing itself when you get the error. You see it has to load a script from the configured script_url. That may take a while. Therefore, you have to make use of a callback such as oninit.
If you do not have control over init method of TinyMCE then, you can follow this solution.
jQuery(document).ready(function($) {
function myCustomSetContent( id, content ) {
// Check if TinyMCE is defined or not.
if( typeof tinymce != "undefined" ) {
var editor = tinymce.get( id );
// Check if TinyMCE is initialized properly or not.
if( editor && editor instanceof tinymce.Editor ) {
editor.setContent( text );
editor.save( { no_events: true } );
} else {
// Fallback
// If TinyMCE is not initialized then directly set the value in textarea.
//TinyMCE will take up this value when it gets initialized.
jQuery( '#'+id ).val( text );
}
return true;
}
return false;
}
function myCustomGetContent( id ) {
// Check if TinyMCE is defined or not.
if( typeof tinymce != "undefined" ) {
var editor = tinymce.get( id );
// Check if TinyMCE is initialized properly or not.
if( editor && editor instanceof tinymce.Editor ) {
return editor.getContent();
} else {
// Fallback
// If TinyMCE is not initialized then directly set the value in textarea.
// TinyMCE will take up this value when it gets initialized.
return jQuery( '#'+id ).val();
}
}
return '';
}
$(".class-to-update-content").on("click", function(e) {
myCustomSetContent( "tinymce-editor-id", "New Content in Editor" );
});
$(".class-to-get-content").on("click", function(e) {
$("div.class-to-display-content").html( myCustomGetContent( "tinymce-editor-id" ) );
});
});
Ref : http://blog.incognitech.in/tinymce-undefined-issue/
EDIT: Solution included

Resources