Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add configurable block and inline styles for Tiptap #51

Merged
merged 6 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("><"))(
fsbraun marked this conversation as resolved.
Show resolved Hide resolved
"<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 = {
fsbraun marked this conversation as resolved.
Show resolved Hide resolved
fsbraun marked this conversation as resolved.
Show resolved Hide resolved
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
Loading