* 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/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", | |||
@@ -0,0 +1,124 @@ | |||
import 'quill-mention'; | |||
frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ | |||
make_wrapper() { | |||
this.comment_wrapper = !this.no_wrapper ? $(` | |||
<div class="comment-input-wrapper"> | |||
<div class="comment-input-header"> | |||
<span class="small text-muted">${__("Add a comment")}</span> | |||
<button class="btn btn-default btn-comment btn-xs pull-right"> | |||
${__("Comment")} | |||
</button> | |||
</div> | |||
<div class="comment-input-container"> | |||
<div class="frappe-control"></div> | |||
<div class="text-muted small"> | |||
${__("Ctrl+Enter to add comment")} | |||
</div> | |||
</div> | |||
</div> | |||
`) : $('<div class="frappe-control"></div>'); | |||
this.comment_wrapper.appendTo(this.parent); | |||
// wrapper should point to frappe-control | |||
this.$wrapper = !this.no_wrapper | |||
? this.comment_wrapper.find('.frappe-control') | |||
: this.comment_wrapper; | |||
this.wrapper = this.$wrapper; | |||
this.button = this.comment_wrapper.find('.btn-comment'); | |||
}, | |||
bind_events() { | |||
this._super(); | |||
this.button.click(() => { | |||
this.submit(); | |||
}); | |||
this.$wrapper.on('keydown', e => { | |||
const key = frappe.ui.keys.get_key(e); | |||
if (key === 'ctrl+enter') { | |||
e.preventDefault(); | |||
this.submit(); | |||
} | |||
}); | |||
this.quill.on('text-change', frappe.utils.debounce(() => { | |||
this.update_state(); | |||
}, 300)); | |||
}, | |||
submit() { | |||
this.on_submit && this.on_submit(this.get_value()); | |||
}, | |||
update_state() { | |||
const value = this.get_value(); | |||
if (strip_html(value)) { | |||
this.button.removeClass('btn-default').addClass('btn-primary'); | |||
} else { | |||
this.button.addClass('btn-default').removeClass('btn-primary'); | |||
} | |||
}, | |||
get_quill_options() { | |||
const options = this._super(); | |||
return Object.assign(options, { | |||
theme: 'bubble', | |||
modules: Object.assign(options.modules, { | |||
mention: this.get_mention_options() | |||
}) | |||
}); | |||
}, | |||
get_mention_options() { | |||
if (!(this.mentions && this.mentions.length)) { | |||
return null; | |||
} | |||
const at_values = this.mentions.map((value, i) => { | |||
return { | |||
id: i, | |||
value | |||
}; | |||
}); | |||
return { | |||
allowedChars: /^[A-Za-z0-9_]*$/, | |||
mentionDenotationChars: ["@"], | |||
isolateCharacter: true, | |||
source: function(searchTerm, renderList, mentionChar) { | |||
let values; | |||
if (mentionChar === "@") { | |||
values = at_values; | |||
} | |||
if (searchTerm.length === 0) { | |||
renderList(values, searchTerm); | |||
} else { | |||
const matches = []; | |||
for (let i = 0; i < values.length; i++) { | |||
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) { | |||
matches.push(values[i]); | |||
} | |||
} | |||
renderList(matches, searchTerm); | |||
} | |||
}, | |||
}; | |||
}, | |||
get_toolbar_options() { | |||
return [ | |||
['bold', 'italic', 'underline'], | |||
['blockquote', 'code-block'], | |||
['link', 'image'], | |||
[{ 'list': 'ordered' }, { 'list': 'bullet' }], | |||
['clean'] | |||
]; | |||
}, | |||
}); |
@@ -1,338 +1,79 @@ | |||
import Quill from 'quill/dist/quill'; | |||
import { ImageDrop } from 'quill-image-drop-module'; | |||
Quill.register('modules/imageDrop', ImageDrop); | |||
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
make_input: function() { | |||
make_input() { | |||
this.has_input = true; | |||
this.make_editor(); | |||
this.hide_elements_on_mobile(); | |||
this.setup_drag_drop(); | |||
this.setup_image_dialog(); | |||
this.setting_count = 0; | |||
$(document).on('form-refresh', () => { | |||
// reset last keystroke when a new form is loaded | |||
this.last_keystroke_on = null; | |||
}) | |||
this.make_quill_editor(); | |||
}, | |||
render_camera_button: (context) => { | |||
var ui = $.summernote.ui; | |||
var button = ui.button({ | |||
contents: '<i class="fa fa-camera"/>', | |||
tooltip: 'Camera', | |||
click: () => { | |||
const capture = new frappe.ui.Capture(); | |||
capture.show(); | |||
capture.submit((data) => { | |||
context.invoke('editor.insertImage', data); | |||
}); | |||
} | |||
}); | |||
return button.render(); | |||
make_quill_editor() { | |||
if (this.quill) return; | |||
this.quill_container = $('<div>').appendTo(this.input_area); | |||
this.quill = new Quill(this.quill_container[0], this.get_quill_options()); | |||
this.bind_events(); | |||
}, | |||
make_editor: function() { | |||
var me = this; | |||
this.editor = $("<div>").appendTo(this.input_area); | |||
// Note: while updating summernote, please make sure all 'p' blocks | |||
// in the summernote source code are replaced by 'div' blocks. | |||
// by default summernote, adds <p> blocks for new paragraphs, which adds | |||
// unexpected whitespaces, esp for email replies. | |||
bind_events() { | |||
this.quill.on('text-change', frappe.utils.debounce(() => { | |||
const input_value = this.get_input_value(); | |||
if (this.value === input_value) return; | |||
this.editor.summernote({ | |||
minHeight: 400, | |||
toolbar: [ | |||
['magic', ['style']], | |||
['style', ['bold', 'italic', 'underline', 'clear']], | |||
['fontsize', ['fontsize']], | |||
['color', ['color']], | |||
['para', ['ul', 'ol', 'paragraph', 'hr']], | |||
//['height', ['height']], | |||
['media', ['link', 'picture', 'camera', 'video', 'table']], | |||
['misc', ['fullscreen', 'codeview']] | |||
], | |||
buttons: { | |||
camera: this.render_camera_button, | |||
}, | |||
keyMap: { | |||
pc: { | |||
'CTRL+ENTER': '' | |||
}, | |||
mac: { | |||
'CMD+ENTER': '' | |||
} | |||
}, | |||
prettifyHtml: true, | |||
dialogsInBody: true, | |||
callbacks: { | |||
onInit: function() { | |||
// firefox hack that puts the caret in the wrong position | |||
// when div is empty. To fix, seed with a <br>. | |||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=550434 | |||
// this function is executed only once | |||
$(".note-editable[contenteditable='true']").one('focus', function() { | |||
var $this = $(this); | |||
if(!$this.html()) | |||
$this.html($this.html() + '<br>'); | |||
}); | |||
}, | |||
onChange: function(value) { | |||
me.parse_validate_and_set_in_model(value); | |||
}, | |||
onKeydown: function(e) { | |||
me.last_keystroke_on = new Date(); | |||
var key = frappe.ui.keys.get_key(e); | |||
// prevent 'New DocType (Ctrl + B)' shortcut in editor | |||
if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) { | |||
e.stopPropagation(); | |||
} | |||
if(key.indexOf('escape') !== -1) { | |||
if(me.note_editor.hasClass('fullscreen')) { | |||
// exit fullscreen on escape key | |||
me.note_editor | |||
.find('.note-btn.btn-fullscreen') | |||
.trigger('click'); | |||
} | |||
} | |||
}, | |||
}, | |||
icons: { | |||
'align': 'fa fa-align', | |||
'alignCenter': 'fa fa-align-center', | |||
'alignJustify': 'fa fa-align-justify', | |||
'alignLeft': 'fa fa-align-left', | |||
'alignRight': 'fa fa-align-right', | |||
'indent': 'fa fa-indent', | |||
'outdent': 'fa fa-outdent', | |||
'arrowsAlt': 'fa fa-arrows-alt', | |||
'bold': 'fa fa-bold', | |||
'camera': 'fa fa-camera', | |||
'caret': 'caret', | |||
'circle': 'fa fa-circle', | |||
'close': 'fa fa-close', | |||
'code': 'fa fa-code', | |||
'eraser': 'fa fa-eraser', | |||
'font': 'fa fa-font', | |||
'frame': 'fa fa-frame', | |||
'italic': 'fa fa-italic', | |||
'link': 'fa fa-link', | |||
'unlink': 'fa fa-chain-broken', | |||
'magic': 'fa fa-magic', | |||
'menuCheck': 'fa fa-check', | |||
'minus': 'fa fa-minus', | |||
'orderedlist': 'fa fa-list-ol', | |||
'pencil': 'fa fa-pencil', | |||
'picture': 'fa fa-image', | |||
'question': 'fa fa-question', | |||
'redo': 'fa fa-redo', | |||
'square': 'fa fa-square', | |||
'strikethrough': 'fa fa-strikethrough', | |||
'subscript': 'fa fa-subscript', | |||
'superscript': 'fa fa-superscript', | |||
'table': 'fa fa-table', | |||
'textHeight': 'fa fa-text-height', | |||
'trash': 'fa fa-trash', | |||
'underline': 'fa fa-underline', | |||
'undo': 'fa fa-undo', | |||
'unorderedlist': 'fa fa-list-ul', | |||
'video': 'fa fa-video-camera' | |||
this.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.comment_area = new frappe.ui.CommentArea({ | |||
this.comment_area = frappe.ui.form.make_control({ | |||
parent: this.wrapper.find('.timeline-head'), | |||
df: { | |||
fieldtype: 'Comment', | |||
fieldname: 'comment', | |||
label: 'Comment' | |||
}, | |||
mentions: this.get_names_for_mentions(), | |||
render_input: true, | |||
only_input: true, | |||
on_submit: (val) => { | |||
val && this.insert_comment( | |||
"Comment", val, this.comment_area.button); | |||
val && this.insert_comment("Comment", val, this.comment_area.button); | |||
} | |||
}); | |||
this.setup_editing_area(); | |||
this.setup_email_button(); | |||
this.list.on("click", ".toggle-blockquote", function() { | |||
@@ -87,25 +91,13 @@ frappe.ui.form.Timeline = Class.extend({ | |||
}); | |||
} else { | |||
$.extend(args, { | |||
txt: frappe.markdown(me.comment_area.val()) | |||
txt: frappe.markdown(me.comment_area.get_value()) | |||
}); | |||
} | |||
new frappe.views.CommunicationComposer(args) | |||
}); | |||
}, | |||
setup_editing_area: function() { | |||
this.$editing_area = $('<div class="timeline-editing-area">'); | |||
this.editing_area = new frappe.ui.CommentArea({ | |||
parent: this.$editing_area, | |||
mentions: this.get_names_for_mentions(), | |||
no_wrapper: true | |||
}); | |||
this.editing_area.destroy(); | |||
}, | |||
refresh: function(scroll_to_end) { | |||
var me = this; | |||
@@ -117,7 +109,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||
} | |||
this.wrapper.toggle(true); | |||
this.list.empty(); | |||
this.comment_area.val(''); | |||
this.comment_area.set_value(''); | |||
var communications = this.get_communications(true); | |||
var views = this.get_view_logs(); | |||
@@ -159,6 +151,21 @@ frappe.ui.form.Timeline = Class.extend({ | |||
this.frm.trigger('timeline_refresh'); | |||
}, | |||
make_editing_area(container) { | |||
return frappe.ui.form.make_control({ | |||
parent: container, | |||
df: { | |||
fieldtype: 'Comment', | |||
fieldname: 'comment', | |||
label: 'Comment' | |||
}, | |||
mentions: this.get_names_for_mentions(), | |||
render_input: true, | |||
only_input: true, | |||
no_wrapper: true | |||
}); | |||
}, | |||
render_timeline_item: function(c) { | |||
var me = this; | |||
this.prepare_timeline_item(c); | |||
@@ -174,22 +181,31 @@ frappe.ui.form.Timeline = Class.extend({ | |||
var name = $timeline_item.data('name'); | |||
if($timeline_item.hasClass('is-editing')) { | |||
me.editing_area.submit(); | |||
me.$editing_area.detach(); | |||
me.current_editing_area.submit(); | |||
} else { | |||
var $edit_btn = $(this); | |||
var content = $timeline_item.find('.timeline-item-content').html(); | |||
const $edit_btn = $(this); | |||
const $timeline_content = $timeline_item.find('.timeline-item-content'); | |||
const $timeline_edit = $timeline_item.find('.timeline-item-edit'); | |||
const content = $timeline_content.html(); | |||
// update state | |||
$edit_btn | |||
.text("Save") | |||
.text(__("Save")) | |||
.find('i') | |||
.removeClass('octicon-pencil') | |||
.addClass('octicon-check'); | |||
$timeline_content.hide(); | |||
$timeline_item.addClass('is-editing'); | |||
// initialize editing area | |||
me.current_editing_area = me.make_editing_area($timeline_edit); | |||
me.current_editing_area.set_value(content); | |||
// submit handler | |||
me.current_editing_area.on_submit = (value) => { | |||
$timeline_edit.empty(); | |||
$timeline_content.show(); | |||
me.editing_area.setup_summernote(); | |||
me.editing_area.val(content); | |||
me.editing_area.on_submit = (value) => { | |||
me.editing_area.destroy(); | |||
value = value.trim(); | |||
// set content to new val so that on save and refresh the new content is shown | |||
c.content = value; | |||
frappe.timeline.update_communication(c); | |||
@@ -197,15 +213,6 @@ frappe.ui.form.Timeline = Class.extend({ | |||
// all changes to the timeline_item for editing are reset after calling refresh | |||
me.refresh(); | |||
}; | |||
$timeline_item | |||
.find('.timeline-item-content') | |||
.hide(); | |||
$timeline_item | |||
.find('.timeline-content-show') | |||
.append(me.$editing_area); | |||
$timeline_item | |||
.addClass('is-editing'); | |||
} | |||
return false; | |||
@@ -570,7 +577,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||
btn: btn, | |||
callback: function(r) { | |||
if(!r.exc) { | |||
me.comment_area.val(''); | |||
me.comment_area.set_value(''); | |||
frappe.utils.play_sound("click"); | |||
var comment = r.message; | |||
@@ -128,6 +128,7 @@ | |||
{%= data.content_html %} | |||
</div> | |||
<div class="timeline-item-edit"></div> | |||
{% if(data.attachments && data.attachments.length) { %} | |||
<div style="margin: 10px 0px"> | |||
{% $.each(data.attachments, function(i, a) { %} | |||
@@ -605,7 +605,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
let subject_field = this.columns[0].df; | |||
let value = doc[subject_field.fieldname] || doc.name; | |||
let subject = strip_html(value); | |||
let escaped_subject = frappe.utils.escape_html(value); | |||
let escaped_subject = frappe.utils.escape_html(subject); | |||
const liked_by = JSON.parse(doc._liked_by || '[]'); | |||
let heart_class = liked_by.includes(user) ? | |||
@@ -1,340 +0,0 @@ | |||
/** | |||
* CommentArea: A small rich text editor with | |||
* support for @mentions and :emojis: | |||
* @example | |||
* let comment_area = new frappe.ui.CommentArea({ | |||
* parent: '.comment-area', | |||
* mentions: ['john', 'mary', 'kate'], | |||
* on_submit: (value) => save_to_database(value) | |||
* }); | |||
*/ | |||
frappe.provide('frappe.ui'); | |||
frappe.provide('frappe.chat'); | |||
frappe.ui.CommentArea = class CommentArea { | |||
constructor({ parent = null, mentions = [], on_submit = null, no_wrapper = false }) { | |||
this.parent = $(parent); | |||
this.mentions = mentions; | |||
this.on_submit = on_submit; | |||
this.no_wrapper = no_wrapper; | |||
this.make(); | |||
// Load emojis initially from https://git.io/frappe-emoji | |||
frappe.chat.emoji(); | |||
// All good. | |||
} | |||
make() { | |||
this.setup_dom(); | |||
this.setup_summernote(); | |||
this.bind_events(); | |||
} | |||
setup_dom() { | |||
const header = !this.no_wrapper ? | |||
`<div class="comment-input-header"> | |||
<span class="small text-muted">${__("Add a comment")}</span> | |||
<button class="btn btn-default btn-comment btn-xs pull-right"> | |||
${__("Comment")} | |||
</button> | |||
</div>` : ''; | |||
const footer = !this.no_wrapper ? | |||
`<div class="text-muted small"> | |||
${__("Ctrl+Enter to add comment")} | |||
</div>` : ''; | |||
this.wrapper = $(` | |||
<div class="comment-input-wrapper"> | |||
${ header } | |||
<div class="comment-input-container"> | |||
<div class="form-control comment-input"></div> | |||
${ footer } | |||
</div> | |||
</div> | |||
`); | |||
this.wrapper.appendTo(this.parent); | |||
this.input = this.parent.find('.comment-input'); | |||
this.button = this.parent.find('.btn-comment'); | |||
} | |||
setup_summernote() { | |||
const { input, button } = this; | |||
input.summernote({ | |||
height: 100, | |||
toolbar: false, | |||
airMode: true, | |||
hint: { | |||
mentions: this.mentions, | |||
match: /\B([@:]\w*)/, | |||
search: function (keyword, callback) { | |||
let items = []; | |||
if (keyword.startsWith('@')) { | |||
keyword = keyword.substr(1); | |||
items = this.mentions; | |||
} else if (keyword.startsWith(':')) { | |||
frappe.chat.emoji(emojis => { // Returns cached, else fetch. | |||
const query = keyword.slice(1); | |||
const items = [ ]; | |||
for (const emoji of emojis) | |||
for (const alias of emoji.aliases) | |||
if ( alias.indexOf(query) === 0 ) | |||
items.push({ emoji: true, name: alias, value: emoji.emoji }); | |||
callback(items); | |||
}); | |||
} | |||
callback($.grep(items, function (item) { | |||
return item.indexOf(keyword) == 0; | |||
})); | |||
}, | |||
template: function (item) { | |||
if ( item.emoji ) { | |||
return item.value + ' ' + item.name; | |||
} else { | |||
return item; | |||
} | |||
}, | |||
content: function (item) { | |||
if ( item.emoji ) { | |||
return item.value; | |||
} else { | |||
return '@' + item; | |||
} | |||
} | |||
}, | |||
callbacks: { | |||
onChange: () => { | |||
this.set_state(); | |||
}, | |||
onKeydown: (e) => { | |||
var key = frappe.ui.keys.get_key(e); | |||
if(key === 'ctrl+enter') { | |||
e.preventDefault(); | |||
this.submit(); | |||
} | |||
e.stopPropagation(); | |||
}, | |||
}, | |||
icons: { | |||
'align': 'fa fa-align', | |||
'alignCenter': 'fa fa-align-center', | |||
'alignJustify': 'fa fa-align-justify', | |||
'alignLeft': 'fa fa-align-left', | |||
'alignRight': 'fa fa-align-right', | |||
'indent': 'fa fa-indent', | |||
'outdent': 'fa fa-outdent', | |||
'arrowsAlt': 'fa fa-arrows-alt', | |||
'bold': 'fa fa-bold', | |||
'caret': 'caret', | |||
'circle': 'fa fa-circle', | |||
'close': 'fa fa-close', | |||
'code': 'fa fa-code', | |||
'eraser': 'fa fa-eraser', | |||
'font': 'fa fa-font', | |||
'frame': 'fa fa-frame', | |||
'italic': 'fa fa-italic', | |||
'link': 'fa fa-link', | |||
'unlink': 'fa fa-chain-broken', | |||
'magic': 'fa fa-magic', | |||
'menuCheck': 'fa fa-check', | |||
'minus': 'fa fa-minus', | |||
'orderedlist': 'fa fa-list-ol', | |||
'pencil': 'fa fa-pencil', | |||
'picture': 'fa fa-image', | |||
'question': 'fa fa-question', | |||
'redo': 'fa fa-redo', | |||
'square': 'fa fa-square', | |||
'strikethrough': 'fa fa-strikethrough', | |||
'subscript': 'fa fa-subscript', | |||
'superscript': 'fa fa-superscript', | |||
'table': 'fa fa-table', | |||
'textHeight': 'fa fa-text-height', | |||
'trash': 'fa fa-trash', | |||
'underline': 'fa fa-underline', | |||
'undo': 'fa fa-undo', | |||
'unorderedlist': 'fa fa-list-ul', | |||
'video': 'fa fa-video-camera' | |||
} | |||
}); | |||
this.note_editor = this.wrapper.find('.note-editor'); | |||
this.note_editor.css({ | |||
'border': '1px solid #ebeff2', | |||
'border-radius': '3px', | |||
'padding': '10px', | |||
'margin-bottom': '10px', | |||
'min-height': '80px', | |||
'cursor': 'text' | |||
}); | |||
this.note_editor.on('click', () => input.summernote('focus')); | |||
} | |||
check_state() { | |||
return !(this.input.summernote('isEmpty')); | |||
} | |||
set_state() { | |||
if(this.check_state()) { | |||
this.button | |||
.removeClass('btn-default') | |||
.addClass('btn-primary'); | |||
} else { | |||
this.button | |||
.removeClass('btn-primary') | |||
.addClass('btn-default'); | |||
} | |||
} | |||
reset() { | |||
this.val(''); | |||
} | |||
destroy() { | |||
this.input.summernote('destroy'); | |||
} | |||
bind_events() { | |||
this.button.on('click', this.submit.bind(this)); | |||
} | |||
val(value) { | |||
// Return html if no value specified | |||
if(value === undefined) { | |||
return this.input.summernote('code'); | |||
} | |||
// Set html if value is specified | |||
this.input.summernote('code', value); | |||
} | |||
submit() { | |||
// Pass comment's value (html) to submit handler | |||
this.on_submit && this.on_submit(this.val()); | |||
} | |||
}; | |||
frappe.ui.ReviewArea = class ReviewArea extends frappe.ui.CommentArea { | |||
setup_dom() { | |||
const header = !this.no_wrapper ? | |||
`<div class="comment-input-header"> | |||
<span class="text-muted">${__("Add your review")}</span> | |||
<button class="btn btn-default btn-comment btn-xs disabled pull-right"> | |||
${__("Submit Review")} | |||
</button> | |||
</div>` : ''; | |||
const footer = !this.no_wrapper ? | |||
`<div class="text-muted"> | |||
${__("Ctrl+Enter to submit")} | |||
</div>` : ''; | |||
const rating_area = !this.no_wrapper ? | |||
`<div class="rating-area text-muted"> | |||
${ __("Your rating: ") } | |||
<i class='fa fa-fw fa-star-o star-icon' data-index=0></i> | |||
<i class='fa fa-fw fa-star-o star-icon' data-index=1></i> | |||
<i class='fa fa-fw fa-star-o star-icon' data-index=2></i> | |||
<i class='fa fa-fw fa-star-o star-icon' data-index=3></i> | |||
<i class='fa fa-fw fa-star-o star-icon' data-index=4></i> | |||
</div>` : ''; | |||
this.wrapper = $(` | |||
<div class="comment-input-wrapper"> | |||
${ header } | |||
<div class="comment-input-container"> | |||
${ rating_area } | |||
<div class="comment-input-body margin-top"> | |||
<input class="form-control review-subject" type="text" | |||
placeholder="${__('Subject')}" | |||
style="border-radius: 3px; border-color: #ebeff2"> | |||
</input> | |||
<div class="form-control comment-input"></div> | |||
${ footer } | |||
</div> | |||
</div> | |||
</div> | |||
`); | |||
this.wrapper.appendTo(this.parent); | |||
this.input = this.parent.find('.comment-input'); | |||
this.subject = this.parent.find('.review-subject'); | |||
this.button = this.parent.find('.btn-comment'); | |||
this.ratingArea = this.parent.find('.rating-area'); | |||
this.rating = 0; | |||
} | |||
input_has_value() { | |||
return !(this.input.summernote('isEmpty') || | |||
this.rating === 0 || !this.subject.val().length); | |||
} | |||
set_state() { | |||
if (this.rating === 0) { | |||
this.parent.find('.comment-input-body').hide(); | |||
} else { | |||
this.parent.find('.comment-input-body').show(); | |||
} | |||
if(this.input_has_value()) { | |||
this.button | |||
.removeClass('btn-default disabled') | |||
.addClass('btn-primary'); | |||
} else { | |||
this.button | |||
.removeClass('btn-primary') | |||
.addClass('btn-default disabled'); | |||
} | |||
} | |||
reset() { | |||
this.set_rating(0); | |||
this.subject.val(''); | |||
this.input.summernote('code', ''); | |||
} | |||
bind_events() { | |||
super.bind_events(); | |||
this.ratingArea.on('click', '.star-icon', (e) => { | |||
let index = $(e.target).attr('data-index'); | |||
this.set_rating(parseInt(index) + 1); | |||
}) | |||
this.subject.on('change', () => { | |||
this.set_state(); | |||
}); | |||
this.set_state(); | |||
} | |||
set_rating(rating) { | |||
this.ratingArea.find('.star-icon').each((i, icon) => { | |||
let star = $(icon); | |||
if(i < rating) { | |||
star.removeClass('fa-star-o'); | |||
star.addClass('fa-star'); | |||
} else { | |||
star.removeClass('fa-star'); | |||
star.addClass('fa-star-o'); | |||
} | |||
}) | |||
this.rating = rating; | |||
this.set_state(); | |||
} | |||
val(value) { | |||
if(value === undefined) { | |||
return { | |||
rating: this.rating, | |||
subject: this.subject.val(), | |||
content: this.input.summernote('code') | |||
} | |||
} | |||
// Set html if value is specified | |||
this.input.summernote('code', value); | |||
} | |||
} |
@@ -1,6 +1,7 @@ | |||
@import "variables.less"; | |||
@import "mixins.less"; | |||
@import "common.less"; | |||
@import "quill.less"; | |||
.nav-pills a, .nav-pills a:hover { | |||
border-bottom: none; | |||
@@ -516,63 +517,6 @@ li.user-progress { | |||
margin-bottom: 24px; | |||
} | |||
// summernote editor | |||
.note-editor { | |||
margin-top: 5px; | |||
&.note-frame { | |||
border-color: @border-color; | |||
} | |||
.btn { | |||
outline: none !important; | |||
} | |||
.dropdown-style > li > a > * { | |||
margin: 0; | |||
} | |||
.fa.fa-check { | |||
color: @text-color !important; | |||
} | |||
.dropdown-menu { | |||
z-index: 100; | |||
max-height: 300px; | |||
overflow: auto; | |||
} | |||
.note-image-input { | |||
height: auto; | |||
} | |||
} | |||
// hide some buttons in modal | |||
.modal .note-editor { | |||
.note-btn-italic, | |||
.note-btn-underline, | |||
[data-original-title="Font Size"], | |||
[data-original-title="Video"], | |||
[data-original-title="Table"] { | |||
display: none; | |||
} | |||
} | |||
.note-hint-popover { | |||
border-radius: 3px; | |||
border-color: @border-color; | |||
padding: 0; | |||
.popover-content { | |||
padding: 0; | |||
} | |||
.note-hint-item { | |||
color: @text-color !important; | |||
padding: 5px 8.8px !important; | |||
} | |||
.note-hint-item.active { | |||
background-color: @btn-bg !important; | |||
} | |||
} | |||
.search-dialog { | |||
.modal-dialog { | |||
width: 768px; | |||
@@ -812,10 +756,6 @@ li.user-progress { | |||
} | |||
} | |||
.note-editor.note-frame .note-editing-area .note-editable { | |||
color: @text-color; | |||
} | |||
.input-area input[type=checkbox] { | |||
margin-left: -20px; | |||
} | |||
@@ -0,0 +1,69 @@ | |||
@import "variables.less"; | |||
@import (less) "quill/dist/quill.snow.css"; | |||
@import (less) "quill/dist/quill.bubble.css"; | |||
@import (less) "quill-mention/src/quill.mention.css"; | |||
.ql-toolbar.ql-snow, .ql-container.ql-snow { | |||
border-color: @border-color; | |||
font-family: inherit; | |||
} | |||
.ql-toolbar.ql-snow { | |||
border-top-left-radius: 4px; | |||
border-top-right-radius: 4px; | |||
background-color: @panel-bg; | |||
padding-bottom: 0; | |||
} | |||
.ql-container.ql-snow { | |||
border-bottom-left-radius: 4px; | |||
border-bottom-right-radius: 4px; | |||
} | |||
.ql-snow .ql-editor { | |||
min-height: 400px; | |||
max-height: 600px; | |||
} | |||
.ql-snow .ql-picker-label { | |||
outline: none; | |||
} | |||
.ql-formats { | |||
margin-bottom: 8px; | |||
} | |||
.ql-bubble .ql-editor { | |||
min-height: 100px; | |||
max-height: 300px; | |||
border: 1px solid @light-border-color; | |||
border-radius: 4px; | |||
} | |||
.ql-mention-list-container { | |||
z-index: 1; | |||
} | |||
.ql-mention-list { | |||
border-radius: 4px; | |||
} | |||
.ql-mention-list-item { | |||
font-size: @text-small; | |||
padding: 10px 12px; | |||
height: initial; | |||
line-height: initial; | |||
&.selected { | |||
background-color: @btn-bg; | |||
} | |||
} | |||
.ql-editor .mention { | |||
height: auto; | |||
width: auto; | |||
border-radius: 10px; | |||
border: 1px solid @light-border-color; | |||
padding: 2px 3px; | |||
background-color: @btn-bg; | |||
} |
@@ -26,6 +26,9 @@ | |||
"jsbarcode": "^3.9.0", | |||
"moment": "^2.20.1", | |||
"moment-timezone": "^0.5.21", | |||
"quill": "^1.3.6", | |||
"quill-image-drop-module": "^1.0.3", | |||
"quill-mention": "^2.0.2", | |||
"redis": "^2.8.0", | |||
"showdown": "^1.8.6", | |||
"socket.io": "^2.0.4", | |||
@@ -45,6 +45,8 @@ function get_rollup_options_for_js(output_file, input_files) { | |||
multi_entry(), | |||
// .html -> .js | |||
frappe_html(), | |||
// ignore css imports | |||
ignore_css(), | |||
// .vue -> .js | |||
vue.default(), | |||
// ES6 -> ES5 | |||
@@ -163,6 +165,21 @@ function get_options_for(app) { | |||
.filter(Boolean); | |||
} | |||
function ignore_css() { | |||
return { | |||
name: 'ignore-css', | |||
transform(code, id) { | |||
if (!['.css', '.scss', '.sass', '.less'].some(ext => id.endsWith(ext))) { | |||
return null; | |||
} | |||
return ` | |||
// ignored ${id} | |||
`; | |||
} | |||
}; | |||
}; | |||
module.exports = { | |||
get_options_for | |||
}; |
@@ -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" | |||