Skip to content

Commit

Permalink
feat: Add configurable styles
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun committed Jan 6, 2025
1 parent 182fabf commit 3ade985
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 29 deletions.
12 changes: 9 additions & 3 deletions djangocms_text/editors.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ def default(self, obj):
"Styles": {
"title": _("Styles"),
},
"InlineStyles": {
"title": _("Styles"),
},
"BlockStyles": {
"title": _("Blocks"),
},
"Font": {
"title": _("Font"),
},
Expand Down Expand Up @@ -367,7 +373,7 @@ def default(self, obj):
DEFAULT_TOOLBAR_CMS = [
["Undo", "Redo"],
["CMSPlugins", "cmswidget", "-", "ShowBlocks"],
["Format", "Styles"],
["Format", "Styles", "InlineStyles", "BlockStyles"],
["TextColor", "Highlight", "BGColor", "-", "PasteText", "PasteFromWord"],
["Maximize"],
[
Expand All @@ -393,7 +399,7 @@ def default(self, obj):
DEFAULT_TOOLBAR_HTMLField = [
["Undo", "Redo"],
["ShowBlocks"],
["Format", "Styles"],
["Format", "Styles", "InlineStyles", "BlockStyles"],
["TextColor", "Highlight", "BGColor", "-", "PasteText", "PasteFromWord"],
["Maximize"],
[
Expand Down Expand Up @@ -439,7 +445,7 @@ def __init__(
config: str,
js: Iterable[str] | None = None,
css: dict | None = None,
admin_css: dict | None = None,
admin_css: Iterable[str] | None = None,
inline_editing: bool = False,
child_plugin_support: bool = False,
):
Expand Down
6 changes: 5 additions & 1 deletion private/js/cms.tiptap.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import TableRow from '@tiptap/extension-table-row';
import {TextAlign, TextAlignOptions} from '@tiptap/extension-text-align';
import TiptapToolbar from "./tiptap_plugins/cms.tiptap.toolbar";

import {TextColor, Small, Var, Kbd, Samp, Highlight, InlineQuote} from "./tiptap_plugins/cms.styles";
import {TextColor, Small, Var, Kbd, Samp, Highlight, InlineQuote, InlineStyle, BlockStyle} from "./tiptap_plugins/cms.styles";
import CmsFormExtension from "./tiptap_plugins/cms.formextension";
import CmsToolbarPlugin from "./tiptap_plugins/cms.toolbar";

Expand Down Expand Up @@ -50,6 +50,7 @@ class CMSTipTapPlugin {
TableCell,
CmsDynLink,
Small, Var, Kbd, Samp, Highlight, InlineQuote,
InlineStyle, BlockStyle,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Expand Down Expand Up @@ -119,6 +120,9 @@ class CMSTipTapPlugin {
save_callback: save_callback,
settings: settings,
toolbar: options.toolbar || options.toolbar_HTMLField,
stylesSet: options.stylesSet,
inlineStyles: options.inlineStyles || [],
blockStyles: options.blockStyles || [],
separator_markup: this.separator_markup,
space_markup: this.space_markup,
});
Expand Down
1 change: 0 additions & 1 deletion private/js/tiptap_plugins/cms.dynlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function DynLinkClickHandler(editor) {

const CmsDynLink = Link.extend({
addAttributes() {
'use strict';
return {
'data-cms-href': {
default: null
Expand Down
7 changes: 3 additions & 4 deletions private/js/tiptap_plugins/cms.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ TiptapToolbar.CMSPlugins.render = renderCmsPluginMenu;

// Common node properties for both inline and block nodes
const cmsPluginNodes = {
atom: true,
draggable: true,

addAttributes() {
'use strict';
return {
Expand All @@ -144,10 +147,6 @@ const cmsPluginNodes = {
};
},

atom: true,

draggable: true,

parseHTML() {
'use strict';
return [
Expand Down
195 changes: 178 additions & 17 deletions private/js/tiptap_plugins/cms.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@
/* jshint esversion: 11 */
/* global document, window, console */

'use strict';

import {Mark, mergeAttributes,} from '@tiptap/core';
import {Mark, Node, mergeAttributes, getAttributes,} from '@tiptap/core';
import TiptapToolbar from "./cms.tiptap.toolbar";


const _markElement = {
addOptions() {
'use strict';

return {
HTMLAttributes: {},
};
},
parseHTML() {
'use strict';

return [
{
tag: this.name.toLowerCase()
Expand All @@ -28,7 +26,6 @@ const _markElement = {
},

addCommands() {
'use strict';
let commands = {};

commands[`set${this.name}`] = () => ({ commands }) => {
Expand Down Expand Up @@ -72,7 +69,6 @@ const InlineQuote = Mark.create({
const Highlight = Mark.create({
name: 'Highlight',
parseHTML() {
'use strict';
return [{tag: 'mark'}];
},
renderHTML({ HTMLAttributes }) {
Expand All @@ -83,7 +79,6 @@ const Highlight = Mark.create({
const TextColor = Mark.create({
name: 'textcolor',
addOptions() {
'use strict';
return {
textColors: {
'text-primary': {name: "Primary"},
Expand All @@ -101,17 +96,13 @@ const TextColor = Mark.create({
},

onCreate() {
'use strict';

if (this.editor.options.textColors) {
// Let editor options overwrite the default colors
this.options.textColors = this.editor.options.textColors;
}
},

addAttributes() {
'use strict';

return {
class: {
default: null,
Expand All @@ -123,8 +114,6 @@ const TextColor = Mark.create({
},

parseHTML() {
'use strict';

return [
{
tag: '*',
Expand All @@ -150,12 +139,10 @@ const TextColor = Mark.create({
},

renderHTML: attributes => {
'use strict';
return ['span', mergeAttributes({}, attributes.HTMLAttributes), 0];
},

addCommands() {
'use strict';
return {
setTextColor: (cls) => ({commands}) => {
if (!(cls in this.options.textColors)) {
Expand All @@ -182,4 +169,178 @@ const TextColor = Mark.create({
}
});

export {TextColor, Small, Var, Kbd, Samp, Highlight, InlineQuote, TextColor as default};

const blockTags = ((str) => str.toUpperCase().substring(1, str.length-1).split("><"))(
"<address><article><aside><blockquote><canvas><dd><div><dl><dt><fieldset><figcaption><figure><footer><form>" +
"<h1><h2><h3><h4><h5><h6><header><hr><li><main><nav><noscript><ol><p><pre><section><table><tfoot><ul><video>"
);

function renderStyleMenu(styles, editor) {
let menu = '';
for (let i = 0; i < styles.length; i++) {
const action = blockTags.includes(styles[i].element.toUpperCase()) ? 'BlockStyles' : 'InlineStyles';
menu += `<button data-action="${action}" data-id="${i}">${styles[i].name}</button>`;
}
return menu;
}

/**
* Represents a utility or configuration object for managing and applying styles.
* Provides methods for adding options and attributes, parsing HTML styles, and rendering
* HTML with specific styles and attributes.
*
* @type {Object}
*
* @property {Function} addOptions
* Adds configuration options for styles. Returns an object with an array of styles.
*
* @property {Function} addAttributes
* Adds default attributes for tags. Returns an object containing default settings
* for `tag` and `attributes`.
*
* @property {Function} parseHTML
* Parses the HTML to match and apply styles based on the context (block or inline).
* Adjusts styles using editor options and validates them against node attributes.
* Returns a mapped array of style objects, including tag and attribute configurations.
*
* @property {Function} renderHTML
* Renders the HTML by merging provided attributes with the default ones. Outputs
* a structured array containing the tag, merged attributes, and content placeholder.
*/
const Style = {
addOptions() {
return { styles: [] };
},

addAttributes() {
return {
tag: { default: null },
attributes: { default: {} },
};
},

parseHTML() {
if (this.name === 'blockstyle') {
if (this.editor.options.stylesSet || this.editor.options.blockStyles) {
// Let editor options overwrite the default styles, stylesSet has preference
this.options.styles = this.editor.options.stylesSet || this.editor.options.blockStyles;
}

} else if (this.editor.options.stylesSet || this.editor.options.inlineStyles) {
// Let editor options overwrite the default styles, inlineStyles has preference
this.options.styles = this.editor.options.inlineStyles || this.editor.options.stylesSet;
console.log("Inline", this.options.styles);

}

return this.options.styles.map(style => {
return {
tag: style.element || '*',
getAttrs: node => {
for (const [key, value] of Object.entries(style.attributes)) {
if (key === 'class') {
if (!(style.attributes?.class || '').split(' ').every(cls => node.classList.contains(cls))) {
return false;
}
} else if (node.getAttribute(key) !== value) {
return false;
}
}
if (style.element) {
return {tag: style.element, attributes: style.attributes};
}
return {attributes: style.attributes}
}
};
});
},

renderHTML({HTMLAttributes}) {
return [HTMLAttributes.tag || this.defaultTag, mergeAttributes({}, HTMLAttributes.attributes), 0];
}
};


const InlineStyle = Mark.create({
name: 'inlinestyle',
defaultTag: 'span',
...Style,

renderHTML: ({HTMLAttributes}) => {
return [HTMLAttributes.tag || 'span', mergeAttributes({}, HTMLAttributes.attributes), 0];
},

addCommands() {
return {
setInlineStyle: (id) => ({commands}) => {
const style = this.options.styles[id];
if (!style) {
return false;
}
return commands.setMark(this.name, {
tag: style.element || this.defaultTag,
attributes: style.attributes,
});
},
unsetInlineStyle: () => ({commands}) => {
return commands.unsetMark(this.name);
},
activeInlineStyle: (id) => ({editor}) => {
const style = this.options.styles[id];
if (!style || !editor.isActive(this.name)) {
return false;
}

const activeAttr = editor.getAttributes(this.name);
if ((activeAttr.tag || style.element) && activeAttr.tag !== style.element) {
return false;
}
if (activeAttr.attributes === style.attributes || !style.attributes) {
return true;
}
return JSON.stringify(activeAttr.attributes) === JSON.stringify(style.attributes);
}
};
}
});

const BlockStyle = Node.create({
name: 'blockstyle',
group: 'block',
content: 'block+',
defaultTag: 'div',
...Style,

addCommands() {
return {
toggleBlockStyle: (id) => ({commands}) => {
const style = this.options.styles[id];
if (!style) {
console.warn("Block style not found");
return false;
}
console.log(style);
return commands.toggleWrap(this.name, {
tag: style.element || 'div',
attributes: style.attributes,
});
},
blockStyleActive: (id) => ({editor}) => {
const style = this.options.styles[id];
if (!style) {
return false;
}
return editor.isActive(this.name, {
tag: style.element,
attributes: style.attributes,
});
}
};
}
});

TiptapToolbar.Styles.items = (editor, builder) => renderStyleMenu(editor.options.stylesSet || [], editor);
TiptapToolbar.InlineStyles.items = (editor, builder) => renderStyleMenu(editor.options.inlineStyles || editor.options.stylesSet || [], editor);
TiptapToolbar.BlockStyles.items = (editor, builder) => renderStyleMenu(editor.options.blockStyles || editor.options.stylesSet || [], editor);

export {TextColor, Small, Var, Kbd, Samp, Highlight, InlineQuote, InlineStyle, BlockStyle, TextColor as default};
Loading

0 comments on commit 3ade985

Please sign in to comment.