').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 = $("
").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
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
.
- // 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() + '
');
- });
- },
- 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
on enter
- //this.set_formatted_input('
');
- },
- 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 : '';
}
});
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index ef23e46da9..1187282953 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -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 = $('
');
-
- 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;
diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html
index 54d1a34d47..558e287175 100755
--- a/frappe/public/js/frappe/form/footer/timeline_item.html
+++ b/frappe/public/js/frappe/form/footer/timeline_item.html
@@ -128,6 +128,7 @@
{%= data.content_html %}
+
{% if(data.attachments && data.attachments.length) { %}
{% $.each(data.attachments, function(i, a) { %}
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 563024707b..68fb67f7ec 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -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) ?
diff --git a/frappe/public/js/frappe/ui/comment.js b/frappe/public/js/frappe/ui/comment.js
deleted file mode 100644
index 1ba73a4349..0000000000
--- a/frappe/public/js/frappe/ui/comment.js
+++ /dev/null
@@ -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 ?
- `` : '';
-
- const footer = !this.no_wrapper ?
- `
- ${__("Ctrl+Enter to add comment")}
-
` : '';
-
- this.wrapper = $(`
-
- `);
- 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 ?
- `` : '';
-
- const footer = !this.no_wrapper ?
- `
- ${__("Ctrl+Enter to submit")}
-
` : '';
-
- const rating_area = !this.no_wrapper ?
- `
- ${ __("Your rating: ") }
-
-
-
-
-
-
` : '';
-
- this.wrapper = $(`
-
- `);
- 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);
- }
-}
diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less
index c0034af1fc..7cfa90073d 100644
--- a/frappe/public/less/desk.less
+++ b/frappe/public/less/desk.less
@@ -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;
}
diff --git a/frappe/public/less/quill.less b/frappe/public/less/quill.less
new file mode 100644
index 0000000000..e6e45e6bb4
--- /dev/null
+++ b/frappe/public/less/quill.less
@@ -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;
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 38f860ddea..76aff59f49 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/rollup/config.js b/rollup/config.js
index 6aba903462..72452b21b1 100644
--- a/rollup/config.js
+++ b/rollup/config.js
@@ -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
};
diff --git a/yarn.lock b/yarn.lock
index 30d4f2bf05..6c63b46bde 100644
--- a/yarn.lock
+++ b/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"