Quill editor (#6159)
* feat(Text Editor): Quill Editor * fix: Add imageDrop module - prevent default events * refactor(Comment): Comment is now frappe control - Use quill's bubble theme for comment control * feat: Support @mentions in comment area - Uses quill-mention package * fix: Use setContents to fix autofocus bug * fix: Spaces to Tabs * fix: Missing semicolon * fix: Fix style * fix: Remove all of summernote - Remove comment.js (use fieldtype: 'Comment') - Add quill styles to webform.css * fix: Replace color/background with indent/outdent
This commit is contained in:
parent
8acdc06180
commit
10534276e5
12 changed files with 378 additions and 763 deletions
|
|
@ -62,6 +62,7 @@
|
|||
"public/js/frappe/form/controls/text.js",
|
||||
"public/js/frappe/form/controls/code.js",
|
||||
"public/js/frappe/form/controls/text_editor.js",
|
||||
"public/js/frappe/form/controls/comment.js",
|
||||
"public/js/frappe/form/controls/check.js",
|
||||
"public/js/frappe/form/controls/image.js",
|
||||
"public/js/frappe/form/controls/attach.js",
|
||||
|
|
@ -91,7 +92,6 @@
|
|||
"public/js/frappe/ui/dialog.js"
|
||||
],
|
||||
"css/desk.min.css": [
|
||||
"public/js/lib/summernote/summernote.css",
|
||||
"public/js/lib/leaflet/leaflet.css",
|
||||
"public/js/lib/leaflet/leaflet.draw.css",
|
||||
"public/js/lib/leaflet/L.Control.Locate.css",
|
||||
|
|
@ -124,7 +124,6 @@
|
|||
"public/js/lib/awesomplete/awesomplete.min.js",
|
||||
"public/js/lib/Sortable.min.js",
|
||||
"public/js/lib/jquery/jquery.hotkeys.js",
|
||||
"public/js/lib/summernote/summernote.js",
|
||||
"public/js/lib/bootstrap.min.js",
|
||||
"node_modules/moment/min/moment-with-locales.min.js",
|
||||
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
|
||||
|
|
@ -352,14 +351,13 @@
|
|||
"js/web_form.min.js": [
|
||||
"public/js/frappe/misc/datetime.js",
|
||||
"website/js/web_form.js",
|
||||
"public/js/lib/summernote/summernote.js",
|
||||
"public/js/lib/datepicker/datepicker.min.js",
|
||||
"public/js/lib/datepicker/datepicker.en.js"
|
||||
],
|
||||
"css/web_form.css": [
|
||||
"public/less/list.less",
|
||||
"website/css/web_form.css",
|
||||
"public/js/lib/summernote/summernote.css"
|
||||
"public/less/quill.less"
|
||||
],
|
||||
"js/print_format_v3.min.js": [
|
||||
"public/js/legacy/layout.js",
|
||||
|
|
|
|||
124
frappe/public/js/frappe/form/controls/comment.js
Normal file
124
frappe/public/js/frappe/form/controls/comment.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'quill-mention';
|
||||
|
||||
frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
||||
make_wrapper() {
|
||||
this.comment_wrapper = !this.no_wrapper ? $(`
|
||||
<div class="comment-input-wrapper">
|
||||
<div class="comment-input-header">
|
||||
<span class="small text-muted">${__("Add a comment")}</span>
|
||||
<button class="btn btn-default btn-comment btn-xs pull-right">
|
||||
${__("Comment")}
|
||||
</button>
|
||||
</div>
|
||||
<div class="comment-input-container">
|
||||
<div class="frappe-control"></div>
|
||||
<div class="text-muted small">
|
||||
${__("Ctrl+Enter to add comment")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`) : $('<div class="frappe-control"></div>');
|
||||
|
||||
this.comment_wrapper.appendTo(this.parent);
|
||||
|
||||
// wrapper should point to frappe-control
|
||||
this.$wrapper = !this.no_wrapper
|
||||
? this.comment_wrapper.find('.frappe-control')
|
||||
: this.comment_wrapper;
|
||||
|
||||
this.wrapper = this.$wrapper;
|
||||
|
||||
this.button = this.comment_wrapper.find('.btn-comment');
|
||||
},
|
||||
|
||||
bind_events() {
|
||||
this._super();
|
||||
|
||||
this.button.click(() => {
|
||||
this.submit();
|
||||
});
|
||||
|
||||
this.$wrapper.on('keydown', e => {
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
if (key === 'ctrl+enter') {
|
||||
e.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
|
||||
this.quill.on('text-change', frappe.utils.debounce(() => {
|
||||
this.update_state();
|
||||
}, 300));
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.on_submit && this.on_submit(this.get_value());
|
||||
},
|
||||
|
||||
update_state() {
|
||||
const value = this.get_value();
|
||||
if (strip_html(value)) {
|
||||
this.button.removeClass('btn-default').addClass('btn-primary');
|
||||
} else {
|
||||
this.button.addClass('btn-default').removeClass('btn-primary');
|
||||
}
|
||||
},
|
||||
|
||||
get_quill_options() {
|
||||
const options = this._super();
|
||||
return Object.assign(options, {
|
||||
theme: 'bubble',
|
||||
modules: Object.assign(options.modules, {
|
||||
mention: this.get_mention_options()
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
get_mention_options() {
|
||||
if (!(this.mentions && this.mentions.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const at_values = this.mentions.map((value, i) => {
|
||||
return {
|
||||
id: i,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
allowedChars: /^[A-Za-z0-9_]*$/,
|
||||
mentionDenotationChars: ["@"],
|
||||
isolateCharacter: true,
|
||||
source: function(searchTerm, renderList, mentionChar) {
|
||||
let values;
|
||||
|
||||
if (mentionChar === "@") {
|
||||
values = at_values;
|
||||
}
|
||||
|
||||
if (searchTerm.length === 0) {
|
||||
renderList(values, searchTerm);
|
||||
} else {
|
||||
const matches = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) {
|
||||
matches.push(values[i]);
|
||||
}
|
||||
}
|
||||
renderList(matches, searchTerm);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
get_toolbar_options() {
|
||||
return [
|
||||
['bold', 'italic', 'underline'],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
['clean']
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
@ -1,338 +1,79 @@
|
|||
import Quill from 'quill/dist/quill';
|
||||
import { ImageDrop } from 'quill-image-drop-module';
|
||||
|
||||
Quill.register('modules/imageDrop', ImageDrop);
|
||||
|
||||
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
||||
make_input: function() {
|
||||
make_input() {
|
||||
this.has_input = true;
|
||||
this.make_editor();
|
||||
this.hide_elements_on_mobile();
|
||||
this.setup_drag_drop();
|
||||
this.setup_image_dialog();
|
||||
this.setting_count = 0;
|
||||
|
||||
$(document).on('form-refresh', () => {
|
||||
// reset last keystroke when a new form is loaded
|
||||
this.last_keystroke_on = null;
|
||||
})
|
||||
this.make_quill_editor();
|
||||
},
|
||||
render_camera_button: (context) => {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="fa fa-camera"/>',
|
||||
tooltip: 'Camera',
|
||||
click: () => {
|
||||
const capture = new frappe.ui.Capture();
|
||||
capture.show();
|
||||
|
||||
capture.submit((data) => {
|
||||
context.invoke('editor.insertImage', data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return button.render();
|
||||
make_quill_editor() {
|
||||
if (this.quill) return;
|
||||
this.quill_container = $('<div>').appendTo(this.input_area);
|
||||
this.quill = new Quill(this.quill_container[0], this.get_quill_options());
|
||||
this.bind_events();
|
||||
},
|
||||
make_editor: function() {
|
||||
var me = this;
|
||||
this.editor = $("<div>").appendTo(this.input_area);
|
||||
|
||||
// Note: while updating summernote, please make sure all 'p' blocks
|
||||
// in the summernote source code are replaced by 'div' blocks.
|
||||
// by default summernote, adds <p> blocks for new paragraphs, which adds
|
||||
// unexpected whitespaces, esp for email replies.
|
||||
bind_events() {
|
||||
this.quill.on('text-change', frappe.utils.debounce(() => {
|
||||
const input_value = this.get_input_value();
|
||||
if (this.value === input_value) return;
|
||||
|
||||
this.editor.summernote({
|
||||
minHeight: 400,
|
||||
toolbar: [
|
||||
['magic', ['style']],
|
||||
['style', ['bold', 'italic', 'underline', 'clear']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph', 'hr']],
|
||||
//['height', ['height']],
|
||||
['media', ['link', 'picture', 'camera', 'video', 'table']],
|
||||
['misc', ['fullscreen', 'codeview']]
|
||||
],
|
||||
buttons: {
|
||||
camera: this.render_camera_button,
|
||||
},
|
||||
keyMap: {
|
||||
pc: {
|
||||
'CTRL+ENTER': ''
|
||||
},
|
||||
mac: {
|
||||
'CMD+ENTER': ''
|
||||
}
|
||||
},
|
||||
prettifyHtml: true,
|
||||
dialogsInBody: true,
|
||||
callbacks: {
|
||||
onInit: function() {
|
||||
// firefox hack that puts the caret in the wrong position
|
||||
// when div is empty. To fix, seed with a <br>.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=550434
|
||||
// this function is executed only once
|
||||
$(".note-editable[contenteditable='true']").one('focus', function() {
|
||||
var $this = $(this);
|
||||
if(!$this.html())
|
||||
$this.html($this.html() + '<br>');
|
||||
});
|
||||
},
|
||||
onChange: function(value) {
|
||||
me.parse_validate_and_set_in_model(value);
|
||||
},
|
||||
onKeydown: function(e) {
|
||||
me.last_keystroke_on = new Date();
|
||||
var key = frappe.ui.keys.get_key(e);
|
||||
// prevent 'New DocType (Ctrl + B)' shortcut in editor
|
||||
if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if(key.indexOf('escape') !== -1) {
|
||||
if(me.note_editor.hasClass('fullscreen')) {
|
||||
// exit fullscreen on escape key
|
||||
me.note_editor
|
||||
.find('.note-btn.btn-fullscreen')
|
||||
.trigger('click');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
'align': 'fa fa-align',
|
||||
'alignCenter': 'fa fa-align-center',
|
||||
'alignJustify': 'fa fa-align-justify',
|
||||
'alignLeft': 'fa fa-align-left',
|
||||
'alignRight': 'fa fa-align-right',
|
||||
'indent': 'fa fa-indent',
|
||||
'outdent': 'fa fa-outdent',
|
||||
'arrowsAlt': 'fa fa-arrows-alt',
|
||||
'bold': 'fa fa-bold',
|
||||
'camera': 'fa fa-camera',
|
||||
'caret': 'caret',
|
||||
'circle': 'fa fa-circle',
|
||||
'close': 'fa fa-close',
|
||||
'code': 'fa fa-code',
|
||||
'eraser': 'fa fa-eraser',
|
||||
'font': 'fa fa-font',
|
||||
'frame': 'fa fa-frame',
|
||||
'italic': 'fa fa-italic',
|
||||
'link': 'fa fa-link',
|
||||
'unlink': 'fa fa-chain-broken',
|
||||
'magic': 'fa fa-magic',
|
||||
'menuCheck': 'fa fa-check',
|
||||
'minus': 'fa fa-minus',
|
||||
'orderedlist': 'fa fa-list-ol',
|
||||
'pencil': 'fa fa-pencil',
|
||||
'picture': 'fa fa-image',
|
||||
'question': 'fa fa-question',
|
||||
'redo': 'fa fa-redo',
|
||||
'square': 'fa fa-square',
|
||||
'strikethrough': 'fa fa-strikethrough',
|
||||
'subscript': 'fa fa-subscript',
|
||||
'superscript': 'fa fa-superscript',
|
||||
'table': 'fa fa-table',
|
||||
'textHeight': 'fa fa-text-height',
|
||||
'trash': 'fa fa-trash',
|
||||
'underline': 'fa fa-underline',
|
||||
'undo': 'fa fa-undo',
|
||||
'unorderedlist': 'fa fa-list-ul',
|
||||
'video': 'fa fa-video-camera'
|
||||
}
|
||||
});
|
||||
this.note_editor = $(this.input_area).find('.note-editor');
|
||||
// to fix <p> on enter
|
||||
//this.set_formatted_input('<div><br></div>');
|
||||
},
|
||||
setup_drag_drop: function() {
|
||||
var me = this;
|
||||
this.note_editor.on('dragenter dragover', false)
|
||||
.on('drop', function(e) {
|
||||
var dataTransfer = e.originalEvent.dataTransfer;
|
||||
this.parse_validate_and_set_in_model(input_value);
|
||||
}, 300));
|
||||
|
||||
if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
|
||||
me.note_editor.focus();
|
||||
|
||||
var files = [].slice.call(dataTransfer.files);
|
||||
|
||||
files.forEach(file => {
|
||||
me.get_image(file, (url) => {
|
||||
me.editor.summernote('insertImage', url, file.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
$(this.quill.root).on('keydown', (e) => {
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
if (['ctrl+b', 'meta+b'].includes(key)) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
get_image: function (fileobj, callback) {
|
||||
var reader = new FileReader();
|
||||
}
|
||||
});
|
||||
|
||||
reader.onload = function() {
|
||||
var dataurl = reader.result;
|
||||
// add filename to dataurl
|
||||
var parts = dataurl.split(",");
|
||||
parts[0] += ";filename=" + fileobj.name;
|
||||
dataurl = parts[0] + ',' + parts[1];
|
||||
callback(dataurl);
|
||||
$(this.quill.root).on('drop', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
|
||||
get_quill_options() {
|
||||
return {
|
||||
modules: {
|
||||
toolbar: this.get_toolbar_options(),
|
||||
imageDrop: true
|
||||
},
|
||||
theme: 'snow'
|
||||
};
|
||||
reader.readAsDataURL(fileobj);
|
||||
},
|
||||
hide_elements_on_mobile: function() {
|
||||
this.note_editor.find('.note-btn-underline,\
|
||||
.note-btn-italic, .note-fontsize,\
|
||||
.note-color, .note-height, .btn-codeview')
|
||||
.addClass('hidden-xs');
|
||||
if($('.toggle-sidebar').is(':visible')) {
|
||||
// disable tooltips on mobile
|
||||
this.note_editor.find('.note-btn')
|
||||
.attr('data-original-title', '');
|
||||
|
||||
get_toolbar_options() {
|
||||
return [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
[{ 'font': [] }],
|
||||
['clean']
|
||||
];
|
||||
},
|
||||
|
||||
parse(value) {
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
},
|
||||
get_input_value: function() {
|
||||
return this.editor? this.editor.summernote('code'): '';
|
||||
},
|
||||
parse: function(value) {
|
||||
if(value == null) value = "";
|
||||
return frappe.dom.remove_script_and_style(value);
|
||||
},
|
||||
set_formatted_input: function(value) {
|
||||
if(value !== this.get_input_value()) {
|
||||
this.set_in_editor(value);
|
||||
}
|
||||
},
|
||||
set_in_editor: function(value) {
|
||||
// set values in editor only if
|
||||
// 1. value not be set in the last 500ms
|
||||
// 2. user has not typed anything in the last 3seconds
|
||||
// ---
|
||||
// we will attempt to cleanup the user's DOM, hence if this happens
|
||||
// in the middle of the user is typing, it creates a lot of issues
|
||||
// also firefox tends to reset the cursor for some reason if the values
|
||||
// are reset
|
||||
|
||||
if(this.setting_count > 2) {
|
||||
// we don't understand how the internal triggers work,
|
||||
// so if someone is setting the value third time in 500ms,
|
||||
// then quit
|
||||
return;
|
||||
}
|
||||
|
||||
this.setting_count += 1;
|
||||
|
||||
let time_since_last_keystroke = moment() - moment(this.last_keystroke_on);
|
||||
|
||||
if(!this.last_keystroke_on || (time_since_last_keystroke > 3000)) {
|
||||
// if 3 seconds have passed since the last keystroke and
|
||||
// we have not set any value in the last 1 second, do this
|
||||
setTimeout(() => this.setting_count = 0, 500);
|
||||
this.editor.summernote('code', value || '');
|
||||
this.last_keystroke_on = null;
|
||||
} else {
|
||||
// user is probably still in the middle of typing
|
||||
// so lets not mess up the html by re-updating it
|
||||
// keep checking every second if our 3 second barrier
|
||||
// has been completed, so that we can refresh the html
|
||||
this._setting_value = setInterval(() => {
|
||||
if(time_since_last_keystroke > 3000) {
|
||||
// 3 seconds done! lets refresh
|
||||
// safe to update
|
||||
if(this.last_value !== this.get_input_value()) {
|
||||
// if not already in sync, reset
|
||||
this.editor.summernote('code', this.last_value || '');
|
||||
}
|
||||
clearInterval(this._setting_value);
|
||||
this._setting_value = null;
|
||||
this.setting_count = 0;
|
||||
|
||||
// clear timestamp of last keystroke
|
||||
this.last_keystroke_on = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
set_focus: function() {
|
||||
return this.editor.summernote('focus');
|
||||
},
|
||||
set_upload_options: function() {
|
||||
var me = this;
|
||||
this.upload_options = {
|
||||
parent: this.image_dialog.get_field("upload_area").$wrapper,
|
||||
args: {},
|
||||
max_width: this.df.max_width,
|
||||
max_height: this.df.max_height,
|
||||
options: "Image",
|
||||
no_socketio: true,
|
||||
btn: this.image_dialog.set_primary_action(__("Insert")),
|
||||
on_no_attach: function() {
|
||||
// if no attachmemts,
|
||||
// check if something is selected
|
||||
var selected = me.image_dialog.get_field("select").get_value();
|
||||
if(selected) {
|
||||
me.editor.summernote('insertImage', selected);
|
||||
me.image_dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please attach a file or set a URL"));
|
||||
}
|
||||
},
|
||||
callback: function(attachment) {
|
||||
me.editor.summernote('insertImage', attachment.file_url, attachment.file_name);
|
||||
me.image_dialog.hide();
|
||||
},
|
||||
onerror: function() {
|
||||
me.image_dialog.hide();
|
||||
}
|
||||
};
|
||||
|
||||
if ("is_private" in this.df) {
|
||||
this.upload_options.is_private = this.df.is_private;
|
||||
}
|
||||
|
||||
if(this.frm) {
|
||||
this.upload_options.args = {
|
||||
from_form: 1,
|
||||
doctype: this.frm.doctype,
|
||||
docname: this.frm.docname
|
||||
};
|
||||
} else {
|
||||
this.upload_options.on_attach = function(fileobj, dataurl) {
|
||||
me.editor.summernote('insertImage', dataurl);
|
||||
me.image_dialog.hide();
|
||||
frappe.hide_progress();
|
||||
};
|
||||
}
|
||||
set_formatted_input(value) {
|
||||
if (!this.quill) return;
|
||||
if (value === this.get_input_value()) return;
|
||||
this.quill.setContents(this.quill.clipboard.convert(value));
|
||||
},
|
||||
|
||||
setup_image_dialog: function() {
|
||||
this.note_editor.find('[data-original-title="Image"]').on('click', () => {
|
||||
if(!this.image_dialog) {
|
||||
this.image_dialog = new frappe.ui.Dialog({
|
||||
title: __("Image"),
|
||||
fields: [
|
||||
{fieldtype:"HTML", fieldname:"upload_area"},
|
||||
{fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
|
||||
{fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
this.image_dialog.show();
|
||||
this.image_dialog.get_field("upload_area").$wrapper.empty();
|
||||
|
||||
// select from existing attachments
|
||||
var attachments = this.frm && this.frm.attachments.get_attachments() || [];
|
||||
var select = this.image_dialog.get_field("select");
|
||||
if(attachments.length) {
|
||||
attachments = $.map(attachments, function(o) { return o.file_url; });
|
||||
select.df.options = [""].concat(attachments);
|
||||
select.toggle(true);
|
||||
this.image_dialog.get_field("or_attach").toggle(true);
|
||||
select.refresh();
|
||||
} else {
|
||||
this.image_dialog.get_field("or_attach").toggle(false);
|
||||
select.toggle(false);
|
||||
}
|
||||
select.$input.val("");
|
||||
|
||||
this.set_upload_options();
|
||||
frappe.upload.make(this.upload_options);
|
||||
});
|
||||
get_input_value() {
|
||||
return this.quill ? this.quill.root.innerHTML : '';
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,17 +15,21 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
|
||||
this.list = this.wrapper.find(".timeline-items");
|
||||
|
||||
this.comment_area = new frappe.ui.CommentArea({
|
||||
this.comment_area = frappe.ui.form.make_control({
|
||||
parent: this.wrapper.find('.timeline-head'),
|
||||
df: {
|
||||
fieldtype: 'Comment',
|
||||
fieldname: 'comment',
|
||||
label: 'Comment'
|
||||
},
|
||||
mentions: this.get_names_for_mentions(),
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
on_submit: (val) => {
|
||||
val && this.insert_comment(
|
||||
"Comment", val, this.comment_area.button);
|
||||
val && this.insert_comment("Comment", val, this.comment_area.button);
|
||||
}
|
||||
});
|
||||
|
||||
this.setup_editing_area();
|
||||
|
||||
this.setup_email_button();
|
||||
|
||||
this.list.on("click", ".toggle-blockquote", function() {
|
||||
|
|
@ -87,25 +91,13 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
});
|
||||
} else {
|
||||
$.extend(args, {
|
||||
txt: frappe.markdown(me.comment_area.val())
|
||||
txt: frappe.markdown(me.comment_area.get_value())
|
||||
});
|
||||
}
|
||||
new frappe.views.CommunicationComposer(args)
|
||||
});
|
||||
},
|
||||
|
||||
setup_editing_area: function() {
|
||||
this.$editing_area = $('<div class="timeline-editing-area">');
|
||||
|
||||
this.editing_area = new frappe.ui.CommentArea({
|
||||
parent: this.$editing_area,
|
||||
mentions: this.get_names_for_mentions(),
|
||||
no_wrapper: true
|
||||
});
|
||||
|
||||
this.editing_area.destroy();
|
||||
},
|
||||
|
||||
refresh: function(scroll_to_end) {
|
||||
var me = this;
|
||||
|
||||
|
|
@ -117,7 +109,7 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
}
|
||||
this.wrapper.toggle(true);
|
||||
this.list.empty();
|
||||
this.comment_area.val('');
|
||||
this.comment_area.set_value('');
|
||||
var communications = this.get_communications(true);
|
||||
var views = this.get_view_logs();
|
||||
|
||||
|
|
@ -159,6 +151,21 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
this.frm.trigger('timeline_refresh');
|
||||
},
|
||||
|
||||
make_editing_area(container) {
|
||||
return frappe.ui.form.make_control({
|
||||
parent: container,
|
||||
df: {
|
||||
fieldtype: 'Comment',
|
||||
fieldname: 'comment',
|
||||
label: 'Comment'
|
||||
},
|
||||
mentions: this.get_names_for_mentions(),
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
no_wrapper: true
|
||||
});
|
||||
},
|
||||
|
||||
render_timeline_item: function(c) {
|
||||
var me = this;
|
||||
this.prepare_timeline_item(c);
|
||||
|
|
@ -174,22 +181,31 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
var name = $timeline_item.data('name');
|
||||
|
||||
if($timeline_item.hasClass('is-editing')) {
|
||||
me.editing_area.submit();
|
||||
me.$editing_area.detach();
|
||||
me.current_editing_area.submit();
|
||||
} else {
|
||||
var $edit_btn = $(this);
|
||||
var content = $timeline_item.find('.timeline-item-content').html();
|
||||
const $edit_btn = $(this);
|
||||
const $timeline_content = $timeline_item.find('.timeline-item-content');
|
||||
const $timeline_edit = $timeline_item.find('.timeline-item-edit');
|
||||
const content = $timeline_content.html();
|
||||
|
||||
// update state
|
||||
$edit_btn
|
||||
.text("Save")
|
||||
.text(__("Save"))
|
||||
.find('i')
|
||||
.removeClass('octicon-pencil')
|
||||
.addClass('octicon-check');
|
||||
$timeline_content.hide();
|
||||
$timeline_item.addClass('is-editing');
|
||||
|
||||
// initialize editing area
|
||||
me.current_editing_area = me.make_editing_area($timeline_edit);
|
||||
me.current_editing_area.set_value(content);
|
||||
|
||||
// submit handler
|
||||
me.current_editing_area.on_submit = (value) => {
|
||||
$timeline_edit.empty();
|
||||
$timeline_content.show();
|
||||
|
||||
me.editing_area.setup_summernote();
|
||||
me.editing_area.val(content);
|
||||
me.editing_area.on_submit = (value) => {
|
||||
me.editing_area.destroy();
|
||||
value = value.trim();
|
||||
// set content to new val so that on save and refresh the new content is shown
|
||||
c.content = value;
|
||||
frappe.timeline.update_communication(c);
|
||||
|
|
@ -197,15 +213,6 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
// all changes to the timeline_item for editing are reset after calling refresh
|
||||
me.refresh();
|
||||
};
|
||||
|
||||
$timeline_item
|
||||
.find('.timeline-item-content')
|
||||
.hide();
|
||||
$timeline_item
|
||||
.find('.timeline-content-show')
|
||||
.append(me.$editing_area);
|
||||
$timeline_item
|
||||
.addClass('is-editing');
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -570,7 +577,7 @@ frappe.ui.form.Timeline = Class.extend({
|
|||
btn: btn,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
me.comment_area.val('');
|
||||
me.comment_area.set_value('');
|
||||
frappe.utils.play_sound("click");
|
||||
|
||||
var comment = r.message;
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@
|
|||
|
||||
{%= data.content_html %}
|
||||
</div>
|
||||
<div class="timeline-item-edit"></div>
|
||||
{% if(data.attachments && data.attachments.length) { %}
|
||||
<div style="margin: 10px 0px">
|
||||
{% $.each(data.attachments, function(i, a) { %}
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
let subject_field = this.columns[0].df;
|
||||
let value = doc[subject_field.fieldname] || doc.name;
|
||||
let subject = strip_html(value);
|
||||
let escaped_subject = frappe.utils.escape_html(value);
|
||||
let escaped_subject = frappe.utils.escape_html(subject);
|
||||
|
||||
const liked_by = JSON.parse(doc._liked_by || '[]');
|
||||
let heart_class = liked_by.includes(user) ?
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
/**
|
||||
* CommentArea: A small rich text editor with
|
||||
* support for @mentions and :emojis:
|
||||
* @example
|
||||
* let comment_area = new frappe.ui.CommentArea({
|
||||
* parent: '.comment-area',
|
||||
* mentions: ['john', 'mary', 'kate'],
|
||||
* on_submit: (value) => save_to_database(value)
|
||||
* });
|
||||
*/
|
||||
|
||||
frappe.provide('frappe.ui');
|
||||
frappe.provide('frappe.chat');
|
||||
|
||||
frappe.ui.CommentArea = class CommentArea {
|
||||
|
||||
constructor({ parent = null, mentions = [], on_submit = null, no_wrapper = false }) {
|
||||
this.parent = $(parent);
|
||||
this.mentions = mentions;
|
||||
this.on_submit = on_submit;
|
||||
this.no_wrapper = no_wrapper;
|
||||
|
||||
this.make();
|
||||
|
||||
// Load emojis initially from https://git.io/frappe-emoji
|
||||
frappe.chat.emoji();
|
||||
// All good.
|
||||
}
|
||||
|
||||
make() {
|
||||
this.setup_dom();
|
||||
this.setup_summernote();
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
setup_dom() {
|
||||
const header = !this.no_wrapper ?
|
||||
`<div class="comment-input-header">
|
||||
<span class="small text-muted">${__("Add a comment")}</span>
|
||||
<button class="btn btn-default btn-comment btn-xs pull-right">
|
||||
${__("Comment")}
|
||||
</button>
|
||||
</div>` : '';
|
||||
|
||||
const footer = !this.no_wrapper ?
|
||||
`<div class="text-muted small">
|
||||
${__("Ctrl+Enter to add comment")}
|
||||
</div>` : '';
|
||||
|
||||
this.wrapper = $(`
|
||||
<div class="comment-input-wrapper">
|
||||
${ header }
|
||||
<div class="comment-input-container">
|
||||
<div class="form-control comment-input"></div>
|
||||
${ footer }
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.wrapper.appendTo(this.parent);
|
||||
this.input = this.parent.find('.comment-input');
|
||||
this.button = this.parent.find('.btn-comment');
|
||||
}
|
||||
|
||||
setup_summernote() {
|
||||
const { input, button } = this;
|
||||
|
||||
input.summernote({
|
||||
height: 100,
|
||||
toolbar: false,
|
||||
airMode: true,
|
||||
hint: {
|
||||
mentions: this.mentions,
|
||||
match: /\B([@:]\w*)/,
|
||||
search: function (keyword, callback) {
|
||||
let items = [];
|
||||
if (keyword.startsWith('@')) {
|
||||
keyword = keyword.substr(1);
|
||||
items = this.mentions;
|
||||
} else if (keyword.startsWith(':')) {
|
||||
frappe.chat.emoji(emojis => { // Returns cached, else fetch.
|
||||
const query = keyword.slice(1);
|
||||
const items = [ ];
|
||||
for (const emoji of emojis)
|
||||
for (const alias of emoji.aliases)
|
||||
if ( alias.indexOf(query) === 0 )
|
||||
items.push({ emoji: true, name: alias, value: emoji.emoji });
|
||||
callback(items);
|
||||
});
|
||||
}
|
||||
|
||||
callback($.grep(items, function (item) {
|
||||
return item.indexOf(keyword) == 0;
|
||||
}));
|
||||
},
|
||||
template: function (item) {
|
||||
if ( item.emoji ) {
|
||||
return item.value + ' ' + item.name;
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
},
|
||||
content: function (item) {
|
||||
if ( item.emoji ) {
|
||||
return item.value;
|
||||
} else {
|
||||
return '@' + item;
|
||||
}
|
||||
}
|
||||
},
|
||||
callbacks: {
|
||||
onChange: () => {
|
||||
this.set_state();
|
||||
},
|
||||
onKeydown: (e) => {
|
||||
var key = frappe.ui.keys.get_key(e);
|
||||
if(key === 'ctrl+enter') {
|
||||
e.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
e.stopPropagation();
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
'align': 'fa fa-align',
|
||||
'alignCenter': 'fa fa-align-center',
|
||||
'alignJustify': 'fa fa-align-justify',
|
||||
'alignLeft': 'fa fa-align-left',
|
||||
'alignRight': 'fa fa-align-right',
|
||||
'indent': 'fa fa-indent',
|
||||
'outdent': 'fa fa-outdent',
|
||||
'arrowsAlt': 'fa fa-arrows-alt',
|
||||
'bold': 'fa fa-bold',
|
||||
'caret': 'caret',
|
||||
'circle': 'fa fa-circle',
|
||||
'close': 'fa fa-close',
|
||||
'code': 'fa fa-code',
|
||||
'eraser': 'fa fa-eraser',
|
||||
'font': 'fa fa-font',
|
||||
'frame': 'fa fa-frame',
|
||||
'italic': 'fa fa-italic',
|
||||
'link': 'fa fa-link',
|
||||
'unlink': 'fa fa-chain-broken',
|
||||
'magic': 'fa fa-magic',
|
||||
'menuCheck': 'fa fa-check',
|
||||
'minus': 'fa fa-minus',
|
||||
'orderedlist': 'fa fa-list-ol',
|
||||
'pencil': 'fa fa-pencil',
|
||||
'picture': 'fa fa-image',
|
||||
'question': 'fa fa-question',
|
||||
'redo': 'fa fa-redo',
|
||||
'square': 'fa fa-square',
|
||||
'strikethrough': 'fa fa-strikethrough',
|
||||
'subscript': 'fa fa-subscript',
|
||||
'superscript': 'fa fa-superscript',
|
||||
'table': 'fa fa-table',
|
||||
'textHeight': 'fa fa-text-height',
|
||||
'trash': 'fa fa-trash',
|
||||
'underline': 'fa fa-underline',
|
||||
'undo': 'fa fa-undo',
|
||||
'unorderedlist': 'fa fa-list-ul',
|
||||
'video': 'fa fa-video-camera'
|
||||
}
|
||||
});
|
||||
|
||||
this.note_editor = this.wrapper.find('.note-editor');
|
||||
this.note_editor.css({
|
||||
'border': '1px solid #ebeff2',
|
||||
'border-radius': '3px',
|
||||
'padding': '10px',
|
||||
'margin-bottom': '10px',
|
||||
'min-height': '80px',
|
||||
'cursor': 'text'
|
||||
});
|
||||
this.note_editor.on('click', () => input.summernote('focus'));
|
||||
}
|
||||
|
||||
check_state() {
|
||||
return !(this.input.summernote('isEmpty'));
|
||||
}
|
||||
|
||||
set_state() {
|
||||
if(this.check_state()) {
|
||||
this.button
|
||||
.removeClass('btn-default')
|
||||
.addClass('btn-primary');
|
||||
} else {
|
||||
this.button
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-default');
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.val('');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.input.summernote('destroy');
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.button.on('click', this.submit.bind(this));
|
||||
}
|
||||
|
||||
val(value) {
|
||||
// Return html if no value specified
|
||||
if(value === undefined) {
|
||||
return this.input.summernote('code');
|
||||
}
|
||||
// Set html if value is specified
|
||||
this.input.summernote('code', value);
|
||||
}
|
||||
|
||||
submit() {
|
||||
// Pass comment's value (html) to submit handler
|
||||
this.on_submit && this.on_submit(this.val());
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.ReviewArea = class ReviewArea extends frappe.ui.CommentArea {
|
||||
setup_dom() {
|
||||
const header = !this.no_wrapper ?
|
||||
`<div class="comment-input-header">
|
||||
<span class="text-muted">${__("Add your review")}</span>
|
||||
<button class="btn btn-default btn-comment btn-xs disabled pull-right">
|
||||
${__("Submit Review")}
|
||||
</button>
|
||||
</div>` : '';
|
||||
|
||||
const footer = !this.no_wrapper ?
|
||||
`<div class="text-muted">
|
||||
${__("Ctrl+Enter to submit")}
|
||||
</div>` : '';
|
||||
|
||||
const rating_area = !this.no_wrapper ?
|
||||
`<div class="rating-area text-muted">
|
||||
${ __("Your rating: ") }
|
||||
<i class='fa fa-fw fa-star-o star-icon' data-index=0></i>
|
||||
<i class='fa fa-fw fa-star-o star-icon' data-index=1></i>
|
||||
<i class='fa fa-fw fa-star-o star-icon' data-index=2></i>
|
||||
<i class='fa fa-fw fa-star-o star-icon' data-index=3></i>
|
||||
<i class='fa fa-fw fa-star-o star-icon' data-index=4></i>
|
||||
</div>` : '';
|
||||
|
||||
this.wrapper = $(`
|
||||
<div class="comment-input-wrapper">
|
||||
${ header }
|
||||
<div class="comment-input-container">
|
||||
${ rating_area }
|
||||
<div class="comment-input-body margin-top">
|
||||
<input class="form-control review-subject" type="text"
|
||||
placeholder="${__('Subject')}"
|
||||
style="border-radius: 3px; border-color: #ebeff2">
|
||||
</input>
|
||||
<div class="form-control comment-input"></div>
|
||||
${ footer }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.wrapper.appendTo(this.parent);
|
||||
this.input = this.parent.find('.comment-input');
|
||||
this.subject = this.parent.find('.review-subject');
|
||||
this.button = this.parent.find('.btn-comment');
|
||||
this.ratingArea = this.parent.find('.rating-area');
|
||||
|
||||
this.rating = 0;
|
||||
}
|
||||
|
||||
input_has_value() {
|
||||
return !(this.input.summernote('isEmpty') ||
|
||||
this.rating === 0 || !this.subject.val().length);
|
||||
}
|
||||
|
||||
set_state() {
|
||||
if (this.rating === 0) {
|
||||
this.parent.find('.comment-input-body').hide();
|
||||
} else {
|
||||
this.parent.find('.comment-input-body').show();
|
||||
}
|
||||
|
||||
if(this.input_has_value()) {
|
||||
this.button
|
||||
.removeClass('btn-default disabled')
|
||||
.addClass('btn-primary');
|
||||
} else {
|
||||
this.button
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-default disabled');
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set_rating(0);
|
||||
this.subject.val('');
|
||||
this.input.summernote('code', '');
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
super.bind_events();
|
||||
this.ratingArea.on('click', '.star-icon', (e) => {
|
||||
let index = $(e.target).attr('data-index');
|
||||
this.set_rating(parseInt(index) + 1);
|
||||
})
|
||||
|
||||
this.subject.on('change', () => {
|
||||
this.set_state();
|
||||
});
|
||||
|
||||
this.set_state();
|
||||
}
|
||||
|
||||
set_rating(rating) {
|
||||
this.ratingArea.find('.star-icon').each((i, icon) => {
|
||||
let star = $(icon);
|
||||
if(i < rating) {
|
||||
star.removeClass('fa-star-o');
|
||||
star.addClass('fa-star');
|
||||
} else {
|
||||
star.removeClass('fa-star');
|
||||
star.addClass('fa-star-o');
|
||||
}
|
||||
})
|
||||
|
||||
this.rating = rating;
|
||||
this.set_state();
|
||||
}
|
||||
|
||||
val(value) {
|
||||
if(value === undefined) {
|
||||
return {
|
||||
rating: this.rating,
|
||||
subject: this.subject.val(),
|
||||
content: this.input.summernote('code')
|
||||
}
|
||||
}
|
||||
// Set html if value is specified
|
||||
this.input.summernote('code', value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
@import "variables.less";
|
||||
@import "mixins.less";
|
||||
@import "common.less";
|
||||
@import "quill.less";
|
||||
|
||||
.nav-pills a, .nav-pills a:hover {
|
||||
border-bottom: none;
|
||||
|
|
@ -516,63 +517,6 @@ li.user-progress {
|
|||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// summernote editor
|
||||
.note-editor {
|
||||
margin-top: 5px;
|
||||
|
||||
&.note-frame {
|
||||
border-color: @border-color;
|
||||
}
|
||||
|
||||
.btn {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.dropdown-style > li > a > * {
|
||||
margin: 0;
|
||||
}
|
||||
.fa.fa-check {
|
||||
color: @text-color !important;
|
||||
}
|
||||
.dropdown-menu {
|
||||
z-index: 100;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
.note-image-input {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// hide some buttons in modal
|
||||
.modal .note-editor {
|
||||
.note-btn-italic,
|
||||
.note-btn-underline,
|
||||
[data-original-title="Font Size"],
|
||||
[data-original-title="Video"],
|
||||
[data-original-title="Table"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-hint-popover {
|
||||
border-radius: 3px;
|
||||
border-color: @border-color;
|
||||
padding: 0;
|
||||
|
||||
.popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.note-hint-item {
|
||||
color: @text-color !important;
|
||||
padding: 5px 8.8px !important;
|
||||
}
|
||||
.note-hint-item.active {
|
||||
background-color: @btn-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
.search-dialog {
|
||||
.modal-dialog {
|
||||
width: 768px;
|
||||
|
|
@ -812,10 +756,6 @@ li.user-progress {
|
|||
}
|
||||
}
|
||||
|
||||
.note-editor.note-frame .note-editing-area .note-editable {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.input-area input[type=checkbox] {
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
|
|
|||
69
frappe/public/less/quill.less
Normal file
69
frappe/public/less/quill.less
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
@import "variables.less";
|
||||
@import (less) "quill/dist/quill.snow.css";
|
||||
@import (less) "quill/dist/quill.bubble.css";
|
||||
@import (less) "quill-mention/src/quill.mention.css";
|
||||
|
||||
.ql-toolbar.ql-snow, .ql-container.ql-snow {
|
||||
border-color: @border-color;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
background-color: @panel-bg;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.ql-snow .ql-editor {
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ql-formats {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ql-bubble .ql-editor {
|
||||
min-height: 100px;
|
||||
max-height: 300px;
|
||||
border: 1px solid @light-border-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ql-mention-list-container {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ql-mention-list {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ql-mention-list-item {
|
||||
font-size: @text-small;
|
||||
padding: 10px 12px;
|
||||
height: initial;
|
||||
line-height: initial;
|
||||
|
||||
&.selected {
|
||||
background-color: @btn-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-editor .mention {
|
||||
height: auto;
|
||||
width: auto;
|
||||
border-radius: 10px;
|
||||
border: 1px solid @light-border-color;
|
||||
padding: 2px 3px;
|
||||
background-color: @btn-bg;
|
||||
}
|
||||
|
|
@ -26,6 +26,9 @@
|
|||
"jsbarcode": "^3.9.0",
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.21",
|
||||
"quill": "^1.3.6",
|
||||
"quill-image-drop-module": "^1.0.3",
|
||||
"quill-mention": "^2.0.2",
|
||||
"redis": "^2.8.0",
|
||||
"showdown": "^1.8.6",
|
||||
"socket.io": "^2.0.4",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ function get_rollup_options_for_js(output_file, input_files) {
|
|||
multi_entry(),
|
||||
// .html -> .js
|
||||
frappe_html(),
|
||||
// ignore css imports
|
||||
ignore_css(),
|
||||
// .vue -> .js
|
||||
vue.default(),
|
||||
// ES6 -> ES5
|
||||
|
|
@ -163,6 +165,21 @@ function get_options_for(app) {
|
|||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function ignore_css() {
|
||||
return {
|
||||
name: 'ignore-css',
|
||||
transform(code, id) {
|
||||
if (!['.css', '.scss', '.sass', '.less'].some(ext => id.endsWith(ext))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `
|
||||
// ignored ${id}
|
||||
`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
get_options_for
|
||||
};
|
||||
|
|
|
|||
55
yarn.lock
55
yarn.lock
|
|
@ -449,6 +449,10 @@ clone@^1.0.2:
|
|||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
|
||||
|
||||
clone@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
|
||||
clusterize.js@^0.18.0:
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/clusterize.js/-/clusterize.js-0.18.1.tgz#a286a9749bd1fa9c2fe21b7fabd8780a590dd836"
|
||||
|
|
@ -713,6 +717,10 @@ decamelize@^1.1.1, decamelize@^1.1.2:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
|
||||
deep-equal@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
|
||||
defined@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
|
||||
|
|
@ -848,6 +856,10 @@ etag@~1.8.1:
|
|||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
|
||||
eventemitter3@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
|
||||
|
||||
execa@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
|
||||
|
|
@ -923,6 +935,10 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
|||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
|
||||
extend@^3.0.1, extend@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
|
||||
extglob@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
|
||||
|
|
@ -937,6 +953,10 @@ fast-deep-equal@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
|
||||
|
||||
fast-diff@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
|
|
@ -2072,6 +2092,10 @@ p-try@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
||||
|
||||
parchment@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"
|
||||
|
||||
parse-glob@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
|
||||
|
|
@ -2548,6 +2572,37 @@ querystring@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
|
||||
quill-delta@^3.6.2:
|
||||
version "3.6.3"
|
||||
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
|
||||
dependencies:
|
||||
deep-equal "^1.0.1"
|
||||
extend "^3.0.2"
|
||||
fast-diff "1.1.2"
|
||||
|
||||
quill-image-drop-module@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/quill-image-drop-module/-/quill-image-drop-module-1.0.3.tgz#0e5ec8329dd67a12126f166b191bf64d2057a7d3"
|
||||
dependencies:
|
||||
quill "^1.2.2"
|
||||
|
||||
quill-mention@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/quill-mention/-/quill-mention-2.0.2.tgz#8e89e1d6b625d2df1b5a04af9338e35b18e91fce"
|
||||
dependencies:
|
||||
quill "^1.3.4"
|
||||
|
||||
quill@^1.2.2, quill@^1.3.4, quill@^1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.6.tgz#99f4de1fee85925a0d7d4163b6d8328f23317a4d"
|
||||
dependencies:
|
||||
clone "^2.1.1"
|
||||
deep-equal "^1.0.1"
|
||||
eventemitter3 "^2.0.3"
|
||||
extend "^3.0.1"
|
||||
parchment "^1.1.4"
|
||||
quill-delta "^3.6.2"
|
||||
|
||||
randomatic@^1.1.3:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue