diff --git a/src/js/core/dom.js b/src/js/core/dom.js
index d763b1c33..d3db017de 100644
--- a/src/js/core/dom.js
+++ b/src/js/core/dom.js
@@ -495,10 +495,15 @@ const isEmpty = (node) => {
if (len === 0) {
return true;
- } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
+ }
+ else if (!isText(node) && len === 1 && node.innerHTML.trim() === blankHTML) {
// ex)
,
return true;
- } else if (lists.all(node.childNodes, isText) && node.innerHTML === '') {
+ }
+ else if (isText(node) && node.textContent.trim() === '') {
+ return true;
+ }
+ else if (lists.all(node.childNodes, isText) && node.innerHTML === '') {
// ex) ,
return true;
}
@@ -1043,14 +1048,18 @@ const insertAfter = (marker, element) => {
*/
const appendChildNodes = (node, children, skipPaddingBlankHtml) => {
lists.each(children, (child) => {
- // special case: appending a pure UL/OL to a LI element creates inaccessible LI element
- // e.g. press enter in last LI which has UL/OL-subelements
- // Therefore, if current node is LI element with no child nodes (text-node) and appending a list, add a br before
- if (!skipPaddingBlankHtml && isLi(node) && node.firstChild === null && isList(child)) {
- node.appendChild(create("br"));
- }
- node.appendChild(child);
+ if (isElement(node)) {
+ // special case: appending a pure UL/OL to a LI element creates inaccessible LI element
+ // e.g. press enter in last LI which has UL/OL-subelements
+ // Therefore, if current node is LI element with no child nodes (text-node) and appending a list, add a br before
+ if (!skipPaddingBlankHtml && isLi(node) && node.firstChild === null && isList(child)) {
+ node.appendChild(create("br"));
+ }
+
+ node.appendChild(child);
+ }
});
+
return node;
}
diff --git a/src/js/module/Codeview.js b/src/js/module/Codeview.js
index 7afc48faa..1d3678789 100644
--- a/src/js/module/Codeview.js
+++ b/src/js/module/Codeview.js
@@ -1,5 +1,4 @@
import dom from '../core/dom';
-import func from '../core/func';
import key from '../core/key';
import range from '../core/range';
import Str from '../core/Str';
diff --git a/src/js/module/CssClass.js b/src/js/module/CssClass.js
new file mode 100644
index 000000000..e4145d476
--- /dev/null
+++ b/src/js/module/CssClass.js
@@ -0,0 +1,340 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import dom from '../core/dom';
+
+const isInlineElement = (el) => dom.isInline(el);
+
+export default class CssClass {
+ constructor(context) {
+ this.context = context;
+
+ this.ui = $.summernote.ui;
+ this.$body = $(document.body);
+ this.$editor = context.layoutInfo.editor;
+ this.options = context.options;
+ this.lang = this.options.langInfo;
+ this.buttons = context.modules.buttons;
+ this.editor = context.modules.editor;
+ this.selection = this.editor.selection;
+
+ this.events = {
+ // This will be called after modules are initialized.
+ 'summernote.init': function (we, e) {
+ //console.log('summernote initialized', we, e);
+ },
+ // This will be called when user releases a key on editable.
+ 'summernote.keyup': function (we, e) {
+ // console.log('summernote keyup', we, e);
+ }
+ };
+ }
+
+ initialize() {
+ this.addFormats();
+ this.initializeButtons();
+
+ $('.note-toolbar', this.$editor).on('click', '.btn-group-cssclass .dropdown-item', (e) => {
+ // Prevent dropdown close
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.refreshDropdown($(e.currentTarget).parent());
+ });
+
+ $('.note-toolbar', this.$editor).on('mousedown', '.btn-group-cssclass > .btn', (e) => {
+ this.refreshDropdown($(e.currentTarget).next());
+ });
+ }
+
+ addFormats() {
+ if (typeof this.options.cssclass === 'undefined') {
+ this.options.cssclass = {};
+ }
+
+ if (typeof this.options.cssclass.classes === 'undefined') {
+ const rgAlert = /^alert(-.+)?$/;
+ const rgBtn = /^btn(-.+)?$/;
+ const rgBg = /^bg-.+$/;
+ const rgTextColor = /^text-(muted|primary|success|danger|warning|info|dark|white)$/;
+ const rgTextAlign = /^text-(start|center|end)$/;
+ const rgDisplay = /^display-[1-4]$/;
+ const rgWidth = /^w-(25|50|75|100)$/;
+ const rgRounded = /^rounded(-.+)?$/;
+
+ this.options.cssclass.classes = {
+ "alert alert-primary": { toggle: rgAlert },
+ "alert alert-secondary": { toggle: rgAlert },
+ "alert alert-success": { toggle: rgAlert },
+ "alert alert-danger": { toggle: rgAlert },
+ "alert alert-warning": { toggle: rgAlert },
+ "alert alert-info": { toggle: rgAlert },
+ "alert alert-light": { toggle: rgAlert },
+ "alert alert-dark": { toggle: rgAlert },
+ "bg-primary": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-secondary": { displayClass: "px-2 py-1", inline: true, toggle: rgBg },
+ "bg-success": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-danger": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-warning": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-info": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-light": { displayClass: "px-2 py-1", inline: true, toggle: rgBg },
+ "bg-dark": { displayClass: "px-2 py-1 text-white", inline: true, toggle: rgBg },
+ "bg-white": { displayClass: "px-2 py-1 border", inline: true, toggle: rgBg },
+ "rtl": { displayClass: "text-uppercase", inline: true, toggle: /^ltr$/ },
+ "ltr": { displayClass: "text-uppercase", inline: true, toggle: /^rtl$/ },
+ "text-muted": { inline: true, toggle: rgTextColor },
+ "text-primary": {inline: true, toggle: rgTextColor },
+ "text-success": {inline: true, toggle: rgTextColor },
+ "text-danger": { inline: true, toggle: rgTextColor },
+ "text-warning": { inline: true, toggle: rgTextColor },
+ "text-info": { inline: true, toggle: rgTextColor },
+ "text-dark": { inline: true, toggle: rgTextColor },
+ "text-white": { displayClass: "bg-gray", inline: true, toggle: rgTextColor },
+ "font-weight-medium": { inline: true },
+ "w-25": { displayClass: "px-2 py-1 bg-light border", toggle: rgWidth },
+ "w-50": { displayClass: "px-2 py-1 bg-light border", toggle: rgWidth },
+ "w-75": { displayClass: "px-2 py-1 bg-light border", toggle: rgWidth },
+ "w-100": { displayClass: "px-2 py-1 bg-light border", toggle: rgWidth },
+ "btn btn-primary": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-secondary": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-success": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-danger": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-warning": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-info": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-light": { inline: true, toggle: rgBtn, predicate: "a" },
+ "btn btn-dark": { inline: true, toggle: rgBtn, predicate: "a" },
+ "rounded-0": { displayClass: "px-2 py-1 bg-light border", toggle: rgRounded },
+ "rounded-1": { displayClass: "px-2 py-1 bg-light border rounded-1", toggle: rgRounded },
+ "rounded-2": { displayClass: "px-2 py-1 bg-light border rounded-2", toggle: rgRounded },
+ "rounded-3": { displayClass: "px-2 py-1 bg-light border rounded-3", toggle: rgRounded },
+ "rounded-4": { displayClass: "px-2 py-2 bg-light border rounded-4", toggle: rgRounded },
+ "rounded-5": { displayClass: "px-2 py-2 bg-light border rounded-5", toggle: rgRounded },
+ "rounded-pill": { displayClass: "px-2 py-1 bg-light border rounded-pill", toggle: rgRounded },
+ "list-unstyled": { },
+ "display-1": { displayClass: "fs-h1", toggle: rgDisplay },
+ "display-2": { displayClass: "fs-h2", toggle: rgDisplay },
+ "display-3": { displayClass: "fs-h3", toggle: rgDisplay },
+ "display-4": { displayClass: "fs-h4", toggle: rgDisplay },
+ "lead": { }
+ };
+ }
+
+ if (typeof this.options.cssclass.imageShapes === 'undefined') {
+ this.options.cssclass.imageShapes = {
+ "img-fluid": { inline: true },
+ "border": { inline: true },
+ "rounded": { toggle: /^(rounded(-.+)?)|img-thumbnail$/, inline: true },
+ "rounded-circle": { toggle: /^(rounded(-.+)?)|img-thumbnail$/, inline: true },
+ "img-thumbnail": { toggle: /^rounded(-.+)?$/, inline: true },
+ "shadow-sm": { toggle: /^(shadow(-.+)?)$/, inline: true },
+ "shadow": { toggle: /^(shadow(-.+)?)$/, inline: true },
+ "shadow-lg": { toggle: /^(shadow(-.+)?)$/, inline: true }
+ };
+ }
+ }
+
+ initializeButtons() {
+ this.context.memo('button.cssclass', () => {
+ return this.ui.buttonGroup({
+ className: 'btn-group-cssclass',
+ children: [
+ this.ui.button({
+ className: 'dropdown-toggle',
+ contents: this.ui.icon("fab fa-css3"), // TODO
+ callback: (btn) => {
+ btn.data("placement", "bottom")
+ .data("trigger", 'hover')
+ .attr("title", this.lang.attrs.cssClass)
+ .tooltip();
+ },
+ data: {
+ toggle: 'dropdown'
+ }
+ }),
+ this.ui.dropdown({
+ className: 'dropdown-cssclass scrollable-menu',
+ items: _.keys(this.options.cssclass.classes),
+ template: (item) => {
+ const obj = this.options.cssclass.classes[item] || {};
+
+ let cssClass = item;
+ if (obj.displayClass) {
+ cssClass += " " + obj.displayClass;
+ }
+ if (!obj.inline) {
+ cssClass += " d-block";
+ }
+
+ const cssStyle = obj.style ? ' style="{0}"'.format(obj.style) : '';
+ return `${item}`;
+ },
+ click: (e, namespace, value) => {
+ e.preventDefault();
+
+ var ddi = $(e.target).closest('[data-value]');
+ value = value || ddi.data('value');
+ var obj = this.options.cssclass.classes[value] || {};
+
+ this.applyClassToSelection(value, obj);
+ }
+ })
+ ]
+ }).render();
+ });
+
+ // Image shape stuff
+ this.context.memo('button.imageShapes', () => {
+ const imageShapes = Object.keys(this.options.cssclass.imageShapes);
+ const button = this.ui.buttonGroup({
+ className: 'btn-group-imageshape',
+ children: [
+ this.ui.button({
+ className: 'dropdown-toggle',
+ contents: this.ui.icon("fab fa-css3"),
+ callback: (btn) => {
+ btn.data("placement", "bottom");
+ btn.data("trigger", "hover");
+ btn.attr("title", this.lang.imageShapes.tooltip);
+ btn.tooltip();
+
+ btn.on('click', (e) => {
+ this.refreshDropdown($(e.currentTarget).next(), $(this.context.layoutInfo.editable.data('target')), true);
+ });
+ },
+ data: {
+ toggle: 'dropdown'
+ }
+ }),
+ this.ui.dropdownCheck({
+ className: 'dropdown-shape',
+ checkClassName: this.options.icons.menuCheck,
+ items: imageShapes,
+ template: (item) => {
+ const index = imageShapes.indexOf(item);
+ return this.lang.imageShapes.tooltipShapeOptions[index];
+ },
+ click: (e) => {
+ e.preventDefault();
+
+ const ddi = $(e.target).closest('[data-value]');
+ const value = ddi.data('value');
+ const obj = this.options.cssclass.imageShapes[value] || {};
+
+ this.applyClassToSelection(value, obj);
+ }
+ })
+ ]
+ });
+
+ return button.render();
+ });
+ }
+
+ applyClassToSelection(value, obj) {
+ const controlNode = $(this.editor.restoreTarget());
+ const sel = this.selection.nativeSelection;
+ let node = $(sel.focusNode.parentElement, ".note-editable");
+ const caret = sel.type === 'None' || sel.type === 'Caret';
+
+ const apply = (el) => {
+ if (el.is('.' + value.replace(' ', '.'))) {
+ // "btn btn-info" > ".btn.btn-info"
+ // Just remove the same style
+ el.removeClass(value);
+ if (!el.attr('class')) {
+ el.removeAttr('class');
+ }
+
+ if (isInlineElement(el[0]) && !el[0].attributes.length) {
+ // Unwrap the node when it is inline and no attributes are present
+ el.replaceWith(el.html());
+ }
+ }
+ else {
+ if (obj.toggle) {
+ // Remove equivalent classes first
+ const classNames = (el.attr('class') || '').split(' ');
+ _.each(classNames, (name) => {
+ if (name && name !== value && obj.toggle.test(name)) {
+ el.removeClass(name);
+ }
+ });
+ }
+
+ el.toggleClass(value);
+ }
+ }
+
+ this.editor.beforeCommand();
+
+ if (controlNode.length) {
+ // Most likely IMG is selected
+ if (obj.inline) {
+ apply(controlNode);
+ }
+ }
+ else {
+ if (!obj.inline) {
+ // Apply a block-style only to a block-level element
+ if (isInlineElement(node[0])) {
+ // Traverse parents until a block-level element is found
+ node = $(dom.closest(node, n => !isInlineElement(n)));
+ // while (node.length && isInlineElement(node[0])) {
+ // node = node.parent();
+ // }
+ }
+
+ if (node.length && !dom.isEditableRoot(node[0])) {
+ apply(node);
+ }
+ }
+ else if (obj.inline && caret) {
+ apply(node);
+ }
+ else if (sel.rangeCount) {
+ const range = sel.getRangeAt(0).cloneRange();
+ const span = $('');
+ range.surroundContents(span[0]);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }
+
+ this.editor.afterCommand();
+ }
+
+ refreshDropdown(drop, node /* selectedNode */, noBubble) {
+ node = node || $(this.selection.nativeSelection.focusNode, ".note-editable");
+
+ drop.find('> .dropdown-item').each(function () {
+ let ddi = $(this),
+ curNode = node,
+ value = ddi.data('value'),
+ //obj = options.cssclass.classes[value] || {},
+ expr = '.' + value.replace(' ', '.'),
+ match = false;
+
+ while (curNode.length) {
+ if (curNode.is(expr)) {
+ match = true;
+ break;
+ }
+
+ if (noBubble) {
+ break;
+ }
+
+ if (dom.isEditableRoot(curNode)) {
+ break;
+ }
+
+ curNode = curNode.parent();
+ }
+
+ ddi.toggleClass('checked', match);
+ });
+ }
+
+ destroy() {
+ // ???
+ }
+}
\ No newline at end of file
diff --git a/src/js/module/HelpDialog.js b/src/js/module/HelpDialog.js
index 6c0b4e832..df92c3f4a 100644
--- a/src/js/module/HelpDialog.js
+++ b/src/js/module/HelpDialog.js
@@ -15,7 +15,7 @@ export default class HelpDialog {
initialize() {
const $container = this.options.dialogsInBody ? this.$body : this.options.container;
const body = [
- '',
+ '
',
'Summernote @@VERSION@@ · ',
'Project · ',
'Issues',
@@ -29,8 +29,8 @@ export default class HelpDialog {
footer: body,
callback: ($node) => {
$node.find('.modal-body,.note-modal-body').css({
- 'max-height': 300,
- 'overflow': 'scroll',
+ 'max-height': 350,
+ 'overflow-y': 'scroll',
});
},
}).render().appendTo($container);
@@ -43,15 +43,18 @@ export default class HelpDialog {
createShortcutList() {
const keyMap = this.options.keyMap[env.isMac ? 'mac' : 'pc'];
- return Object.keys(keyMap).map((key) => {
+
+ const $root = $('
');
+ const $list = $root.find('.help-list');
+
+ Object.keys(keyMap).forEach(key => {
const command = keyMap[key];
- const $row = $('');
- $row.append($('').css({
- 'width': 180,
- 'margin-right': 10,
- })).append($('').html(this.context.memo('help.' + command) || command));
- return $row.html();
- }).join('');
+ $list
+ .append($('' + key + '
'))
+ .append($('').html(this.context.memo('help.' + command) || command));
+ });
+
+ return $root.html();
}
/**
diff --git a/src/js/settings.js b/src/js/settings.js
index 72f3feccd..12fb687f1 100644
--- a/src/js/settings.js
+++ b/src/js/settings.js
@@ -29,6 +29,7 @@ import VideoDialog from './module/VideoDialog';
import HelpDialog from './module/HelpDialog';
//import AirPopover from './module/AirPopover';
import HintPopover from './module/HintPopover';
+import CssClass from './module/CssClass';
$.summernote = $.extend($.summernote, {
version: '@@VERSION@@',
@@ -56,6 +57,7 @@ $.summernote = $.extend($.summernote, {
'autoSync': AutoSync,
'autoReplace': AutoReplace,
'placeholder': Placeholder,
+ 'cssclass': CssClass,
'buttons': Buttons,
'toolbar': Toolbar,
'linkDialog': LinkDialog,
diff --git a/src/styles/sm/js/globalinit.js b/src/styles/sm/js/globalinit.js
index 8509b68dc..40a97231f 100644
--- a/src/styles/sm/js/globalinit.js
+++ b/src/styles/sm/js/globalinit.js
@@ -75,7 +75,7 @@ let summernote_image_upload_url;
['edit', ['undo', 'redo']],
['text', ['bold', 'italic', 'underline', 'moreFontStyles']],
//['color', ['forecolor', 'backcolor']],
- ['font', ['fontname', 'color', 'fontsize']],
+ //['font', ['fontname', 'color', 'fontsize']],
['para', ['style', 'cssclass', 'ul', 'ol', 'paragraph', 'clear']],
['insert', ['link', 'image', 'video', 'table', 'hr']],
['view', ['codeview', 'fullscreen', 'help']]