* 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/outdentversion-14
@@ -62,6 +62,7 @@ | |||||
"public/js/frappe/form/controls/text.js", | "public/js/frappe/form/controls/text.js", | ||||
"public/js/frappe/form/controls/code.js", | "public/js/frappe/form/controls/code.js", | ||||
"public/js/frappe/form/controls/text_editor.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/check.js", | ||||
"public/js/frappe/form/controls/image.js", | "public/js/frappe/form/controls/image.js", | ||||
"public/js/frappe/form/controls/attach.js", | "public/js/frappe/form/controls/attach.js", | ||||
@@ -91,7 +92,6 @@ | |||||
"public/js/frappe/ui/dialog.js" | "public/js/frappe/ui/dialog.js" | ||||
], | ], | ||||
"css/desk.min.css": [ | "css/desk.min.css": [ | ||||
"public/js/lib/summernote/summernote.css", | |||||
"public/js/lib/leaflet/leaflet.css", | "public/js/lib/leaflet/leaflet.css", | ||||
"public/js/lib/leaflet/leaflet.draw.css", | "public/js/lib/leaflet/leaflet.draw.css", | ||||
"public/js/lib/leaflet/L.Control.Locate.css", | "public/js/lib/leaflet/L.Control.Locate.css", | ||||
@@ -124,7 +124,6 @@ | |||||
"public/js/lib/awesomplete/awesomplete.min.js", | "public/js/lib/awesomplete/awesomplete.min.js", | ||||
"public/js/lib/Sortable.min.js", | "public/js/lib/Sortable.min.js", | ||||
"public/js/lib/jquery/jquery.hotkeys.js", | "public/js/lib/jquery/jquery.hotkeys.js", | ||||
"public/js/lib/summernote/summernote.js", | |||||
"public/js/lib/bootstrap.min.js", | "public/js/lib/bootstrap.min.js", | ||||
"node_modules/moment/min/moment-with-locales.min.js", | "node_modules/moment/min/moment-with-locales.min.js", | ||||
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", | "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", | ||||
@@ -352,14 +351,13 @@ | |||||
"js/web_form.min.js": [ | "js/web_form.min.js": [ | ||||
"public/js/frappe/misc/datetime.js", | "public/js/frappe/misc/datetime.js", | ||||
"website/js/web_form.js", | "website/js/web_form.js", | ||||
"public/js/lib/summernote/summernote.js", | |||||
"public/js/lib/datepicker/datepicker.min.js", | "public/js/lib/datepicker/datepicker.min.js", | ||||
"public/js/lib/datepicker/datepicker.en.js" | "public/js/lib/datepicker/datepicker.en.js" | ||||
], | ], | ||||
"css/web_form.css": [ | "css/web_form.css": [ | ||||
"public/less/list.less", | "public/less/list.less", | ||||
"website/css/web_form.css", | "website/css/web_form.css", | ||||
"public/js/lib/summernote/summernote.css" | |||||
"public/less/quill.less" | |||||
], | ], | ||||
"js/print_format_v3.min.js": [ | "js/print_format_v3.min.js": [ | ||||
"public/js/legacy/layout.js", | "public/js/legacy/layout.js", | ||||
@@ -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({ | frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | ||||
make_input: function() { | |||||
make_input() { | |||||
this.has_input = true; | 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.parse_validate_and_set_in_model(input_value); | |||||
}, 300)); | |||||
$(this.quill.root).on('keydown', (e) => { | |||||
const key = frappe.ui.keys.get_key(e); | |||||
if (['ctrl+b', 'meta+b'].includes(key)) { | |||||
e.stopPropagation(); | |||||
} | } | ||||
}); | }); | ||||
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; | |||||
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(); | |||||
e.stopPropagation(); | |||||
}); | |||||
$(this.quill.root).on('drop', (e) => { | |||||
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); | |||||
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_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'); | |||||
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'] | |||||
]; | |||||
}, | }, | ||||
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(); | |||||
}; | |||||
parse(value) { | |||||
if (value == null) { | |||||
value = ""; | |||||
} | } | ||||
return frappe.dom.remove_script_and_style(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(""); | |||||
set_formatted_input(value) { | |||||
if (!this.quill) return; | |||||
if (value === this.get_input_value()) return; | |||||
this.quill.setContents(this.quill.clipboard.convert(value)); | |||||
}, | |||||
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.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'), | parent: this.wrapper.find('.timeline-head'), | ||||
df: { | |||||
fieldtype: 'Comment', | |||||
fieldname: 'comment', | |||||
label: 'Comment' | |||||
}, | |||||
mentions: this.get_names_for_mentions(), | mentions: this.get_names_for_mentions(), | ||||
render_input: true, | |||||
only_input: true, | |||||
on_submit: (val) => { | 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.setup_email_button(); | ||||
this.list.on("click", ".toggle-blockquote", function() { | this.list.on("click", ".toggle-blockquote", function() { | ||||
@@ -87,25 +91,13 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
}); | }); | ||||
} else { | } else { | ||||
$.extend(args, { | $.extend(args, { | ||||
txt: frappe.markdown(me.comment_area.val()) | |||||
txt: frappe.markdown(me.comment_area.get_value()) | |||||
}); | }); | ||||
} | } | ||||
new frappe.views.CommunicationComposer(args) | 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) { | refresh: function(scroll_to_end) { | ||||
var me = this; | var me = this; | ||||
@@ -117,7 +109,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
} | } | ||||
this.wrapper.toggle(true); | this.wrapper.toggle(true); | ||||
this.list.empty(); | this.list.empty(); | ||||
this.comment_area.val(''); | |||||
this.comment_area.set_value(''); | |||||
var communications = this.get_communications(true); | var communications = this.get_communications(true); | ||||
var views = this.get_view_logs(); | var views = this.get_view_logs(); | ||||
@@ -159,6 +151,21 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
this.frm.trigger('timeline_refresh'); | 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) { | render_timeline_item: function(c) { | ||||
var me = this; | var me = this; | ||||
this.prepare_timeline_item(c); | this.prepare_timeline_item(c); | ||||
@@ -174,22 +181,31 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
var name = $timeline_item.data('name'); | var name = $timeline_item.data('name'); | ||||
if($timeline_item.hasClass('is-editing')) { | if($timeline_item.hasClass('is-editing')) { | ||||
me.editing_area.submit(); | |||||
me.$editing_area.detach(); | |||||
me.current_editing_area.submit(); | |||||
} else { | } 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 | $edit_btn | ||||
.text("Save") | |||||
.text(__("Save")) | |||||
.find('i') | .find('i') | ||||
.removeClass('octicon-pencil') | .removeClass('octicon-pencil') | ||||
.addClass('octicon-check'); | .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 | // set content to new val so that on save and refresh the new content is shown | ||||
c.content = value; | c.content = value; | ||||
frappe.timeline.update_communication(c); | 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 | // all changes to the timeline_item for editing are reset after calling refresh | ||||
me.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; | return false; | ||||
@@ -570,7 +577,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
btn: btn, | btn: btn, | ||||
callback: function(r) { | callback: function(r) { | ||||
if(!r.exc) { | if(!r.exc) { | ||||
me.comment_area.val(''); | |||||
me.comment_area.set_value(''); | |||||
frappe.utils.play_sound("click"); | frappe.utils.play_sound("click"); | ||||
var comment = r.message; | var comment = r.message; | ||||
@@ -128,6 +128,7 @@ | |||||
{%= data.content_html %} | {%= data.content_html %} | ||||
</div> | </div> | ||||
<div class="timeline-item-edit"></div> | |||||
{% if(data.attachments && data.attachments.length) { %} | {% if(data.attachments && data.attachments.length) { %} | ||||
<div style="margin: 10px 0px"> | <div style="margin: 10px 0px"> | ||||
{% $.each(data.attachments, function(i, a) { %} | {% $.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 subject_field = this.columns[0].df; | ||||
let value = doc[subject_field.fieldname] || doc.name; | let value = doc[subject_field.fieldname] || doc.name; | ||||
let subject = strip_html(value); | 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 || '[]'); | const liked_by = JSON.parse(doc._liked_by || '[]'); | ||||
let heart_class = liked_by.includes(user) ? | 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 "variables.less"; | ||||
@import "mixins.less"; | @import "mixins.less"; | ||||
@import "common.less"; | @import "common.less"; | ||||
@import "quill.less"; | |||||
.nav-pills a, .nav-pills a:hover { | .nav-pills a, .nav-pills a:hover { | ||||
border-bottom: none; | border-bottom: none; | ||||
@@ -516,63 +517,6 @@ li.user-progress { | |||||
margin-bottom: 24px; | 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 { | .search-dialog { | ||||
.modal-dialog { | .modal-dialog { | ||||
width: 768px; | 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] { | .input-area input[type=checkbox] { | ||||
margin-left: -20px; | margin-left: -20px; | ||||
} | } | ||||
@@ -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", | "jsbarcode": "^3.9.0", | ||||
"moment": "^2.20.1", | "moment": "^2.20.1", | ||||
"moment-timezone": "^0.5.21", | "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", | "redis": "^2.8.0", | ||||
"showdown": "^1.8.6", | "showdown": "^1.8.6", | ||||
"socket.io": "^2.0.4", | "socket.io": "^2.0.4", | ||||
@@ -45,6 +45,8 @@ function get_rollup_options_for_js(output_file, input_files) { | |||||
multi_entry(), | multi_entry(), | ||||
// .html -> .js | // .html -> .js | ||||
frappe_html(), | frappe_html(), | ||||
// ignore css imports | |||||
ignore_css(), | |||||
// .vue -> .js | // .vue -> .js | ||||
vue.default(), | vue.default(), | ||||
// ES6 -> ES5 | // ES6 -> ES5 | ||||
@@ -163,6 +165,21 @@ function get_options_for(app) { | |||||
.filter(Boolean); | .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 = { | module.exports = { | ||||
get_options_for | get_options_for | ||||
}; | }; |
@@ -449,6 +449,10 @@ clone@^1.0.2: | |||||
version "1.0.3" | version "1.0.3" | ||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" | 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: | clusterize.js@^0.18.0: | ||||
version "0.18.1" | version "0.18.1" | ||||
resolved "https://registry.yarnpkg.com/clusterize.js/-/clusterize.js-0.18.1.tgz#a286a9749bd1fa9c2fe21b7fabd8780a590dd836" | 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" | version "1.2.0" | ||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" | 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: | defined@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" | ||||
@@ -848,6 +856,10 @@ etag@~1.8.1: | |||||
version "1.8.1" | version "1.8.1" | ||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" | 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: | execa@^0.7.0: | ||||
version "0.7.0" | version "0.7.0" | ||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" | 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" | version "3.0.1" | ||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" | 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: | extglob@^0.3.1: | ||||
version "0.3.2" | version "0.3.2" | ||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" | 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" | version "1.1.0" | ||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" | 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: | fast-json-stable-stringify@^2.0.0: | ||||
version "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" | 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" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" | 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: | parse-glob@^3.0.4: | ||||
version "3.0.4" | version "3.0.4" | ||||
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" | 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" | version "0.2.0" | ||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" | 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: | randomatic@^1.1.3: | ||||
version "1.1.7" | version "1.1.7" | ||||
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" | resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" | ||||