Skip to content

Commit

Permalink
CssClass module
Browse files Browse the repository at this point in the history
  • Loading branch information
muratcakir committed Nov 29, 2024
1 parent 971a522 commit 607f2ce
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 22 deletions.
27 changes: 18 additions & 9 deletions src/js/core/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) <p><br></p>, <span><br></span>
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) <p></p>, <span></span>
return true;
}
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 0 additions & 1 deletion src/js/module/Codeview.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
340 changes: 340 additions & 0 deletions src/js/module/CssClass.js
Original file line number Diff line number Diff line change
@@ -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 `<span class="${cssClass}" title="${item}"${cssStyle}>${item}</span>`;
},
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 = $('<span class="' + value + '"></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() {
// ???
}
}
Loading

0 comments on commit 607f2ce

Please sign in to comment.