Pārlūkot izejas kodu

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
version-14
Faris Ansari pirms 6 gadiem
committed by Rushabh Mehta
vecāks
revīzija
10534276e5
12 mainītis faili ar 375 papildinājumiem un 760 dzēšanām
  1. +2
    -4
      frappe/public/build.json
  2. +124
    -0
      frappe/public/js/frappe/form/controls/comment.js
  3. +56
    -315
      frappe/public/js/frappe/form/controls/text_editor.js
  4. +46
    -39
      frappe/public/js/frappe/form/footer/timeline.js
  5. +1
    -0
      frappe/public/js/frappe/form/footer/timeline_item.html
  6. +1
    -1
      frappe/public/js/frappe/list/list_view.js
  7. +0
    -340
      frappe/public/js/frappe/ui/comment.js
  8. +1
    -61
      frappe/public/less/desk.less
  9. +69
    -0
      frappe/public/less/quill.less
  10. +3
    -0
      package.json
  11. +17
    -0
      rollup/config.js
  12. +55
    -0
      yarn.lock

+ 2
- 4
frappe/public/build.json Parādīt failu

@@ -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
- 0
frappe/public/js/frappe/form/controls/comment.js Parādīt failu

@@ -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']
];
},
});

+ 56
- 315
frappe/public/js/frappe/form/controls/text_editor.js Parādīt failu

@@ -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.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 : '';
}
});

+ 46
- 39
frappe/public/js/frappe/form/footer/timeline.js Parādīt failu

@@ -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;


+ 1
- 0
frappe/public/js/frappe/form/footer/timeline_item.html Parādīt failu

@@ -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) { %}


+ 1
- 1
frappe/public/js/frappe/list/list_view.js Parādīt failu

@@ -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) ?


+ 0
- 340
frappe/public/js/frappe/ui/comment.js Parādīt failu

@@ -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
- 61
frappe/public/less/desk.less Parādīt failu

@@ -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
- 0
frappe/public/less/quill.less Parādīt failu

@@ -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;
}

+ 3
- 0
package.json Parādīt failu

@@ -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",


+ 17
- 0
rollup/config.js Parādīt failu

@@ -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
- 0
yarn.lock Parādīt failu

@@ -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"


Notiek ielāde…
Atcelt
Saglabāt