Skip to content

Commit bb753d7

Browse files
committed
Aggiunti stili di testo e toolbar
1 parent 24dc53b commit bb753d7

9 files changed

Lines changed: 269 additions & 39 deletions

File tree

CHANGELOG_TEXT_STYLES.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Feature: Word-like Custom Text Styles & Semantic Integration
2+
3+
## Overview
4+
5+
Implemented a dynamic, fully functioning "Custom Text Styles" engine for the Tiptap editor to allow users to save and easily apply predefined rich CSS formats. Furthermore, integrated the semantic logic (H1-H6, Blockquote, etc.) so that applying styles maintains proper document structure and Table of Contents (TOC) mappings.
6+
7+
## Key Changes
8+
9+
### 1. Dynamic Settings Configuration (`Prefs.ts` & `SettingsView.tsx`)
10+
11+
- Migrated hard-coded CSS styles into the application's global `Prefs` (`EditorPrefs`), allowing styles to be dynamically configurable entirely by the end user.
12+
- Added a `Custom Text Styles` settings section under Editor Options, equipped with a dedicated JSON `Textarea`.
13+
- Created robust UI state management to prevent typing clobbering, enforcing strict JSON checks without disturbing the user's cursor while editing.
14+
15+
### 2. Tiptap Engine Semantic Adaptation (`CustomStyle.ts`)
16+
17+
- Rebuilt the `CustomStyle` Tiptap extension to read styles directly on the fly.
18+
- Taught the `CustomStyle` parser to strip out internal semantic keywords (like `"tag"`) directly from CSS conversion (`renderHTML`).
19+
- Configured dynamic initialization and destruction through `EditorExtensions.ts` and `EditorView.tsx`.
20+
21+
### 3. Smart Unified Toolbar (`Toolbar.tsx`)
22+
23+
- Removed the old, redundant, and messy 1-to-6 heading split component from the Top Toolbar to centralize styling.
24+
- Bound the internal behavior of the Styles `<Select>` element to dynamically execute core Tiptap structural commands (`editor.chain().focus().setHeading({ level: 1 }) `... etc.) depending entirely on the matched user-defined `"tag"` attribute within their configurations.
25+
- Seamlessly reverts standard `setParagraph` toggling on `Default` reset.
26+
27+
### 4. Table of Contents Scroll Upgrade (`TableOfContents.tsx`)
28+
29+
- Wrapped the automatic index nodes (Anchors) with a Mantine `<ScrollArea>` and assigned a fixed responsive height (`h={300}`) to explicitly unlock the vertical scrollbar.
30+
- Re-engineered the TOC overlay wrapper using CSS `resize: "both"` and `display: "flex"`, allowing the user to seamlessly resize the entire box with the mouse.
31+
- Ensured the inner scroll area resizes flawlessly (`flexGrow: 1`) alongside the wrapper, enforcing clean `minWidth`/`maxWidth`/`height` containment settings.

packages/common/Prefs.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,43 @@ class EditorPrefs {
2121
recentCodeLangs: string[] = [];
2222
codeWordWrap = false;
2323
tabSize = 4;
24+
customTextStyles: Record<string, Record<string, string>> = {
25+
Normal: {
26+
"tag": "p",
27+
"font-size": "default",
28+
"font-weight": "normal",
29+
"font-style": "normal",
30+
"color": "inherit"
31+
},
32+
"Heading 1": {
33+
"tag": "h1",
34+
"font-size": "32px",
35+
"font-weight": "bold",
36+
},
37+
"Heading 2": {
38+
"tag": "h2",
39+
"font-size": "24px",
40+
"font-weight": "bold",
41+
},
42+
Subtitle: {
43+
"tag": "p",
44+
"font-size": "18px",
45+
"color": "gray",
46+
"font-style": "italic",
47+
},
48+
Quote: {
49+
"tag": "blockquote",
50+
"font-size": "16px",
51+
"font-style": "italic",
52+
"background-color": "#f0f0f0",
53+
"padding": "4px 8px",
54+
"border-left": "4px solid #ccc"
55+
},
56+
Highlight: {
57+
"background-color": "#ffff00",
58+
"color": "#000000"
59+
}
60+
};
2461
}
2562

2663
class MiscPrefs {

packages/renderer/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function App() {
3535
const fakeEditor = useRef<Editor>(
3636
new Editor({
3737
editable: false,
38-
extensions: extensions({ useTypography: false, tabSize: 4 })
38+
extensions: extensions({ useTypography: false, tabSize: 4, customStyles: {} })
3939
})
4040
);
4141

packages/renderer/src/components/Views/Editor/EditorExtensions.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import { CustomLink } from "./extensions/CustomLink";
2626
import { CustomCode } from "./extensions/CustomCode";
2727
import { CustomTable } from "./extensions/CustomTable";
2828
import { ResizableImage } from "./extensions/ResizableImage/ResizableImage";
29+
import { CustomStyle } from "./extensions/CustomStyle";
2930

30-
export function extensions(options: { useTypography: boolean; tabSize: number }) {
31+
export function extensions(options: { useTypography: boolean; tabSize: number; customStyles: Record<string, Record<string, string>> }) {
3132
const e = [
3233
StarterKit.configure({
3334
codeBlock: false,
@@ -85,7 +86,10 @@ export function extensions(options: { useTypography: boolean; tabSize: number })
8586
Markdown.configure({
8687
html: true
8788
}),
88-
FontSize
89+
FontSize,
90+
CustomStyle.configure({
91+
customStyles: options.customStyles
92+
})
8993
] as Extensions;
9094

9195
if (options.useTypography) e.push(Typography);

packages/renderer/src/components/Views/Editor/EditorView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export function EditorView({ page, setEditorRef }: Props) {
2121
() =>
2222
extensions({
2323
useTypography: appContext.prefs.editor.useTypographyExtension,
24-
tabSize: appContext.prefs.editor.tabSize
24+
tabSize: appContext.prefs.editor.tabSize,
25+
customStyles: appContext.prefs.editor.customTextStyles
2526
}),
26-
[appContext.prefs.editor.tabSize, appContext.prefs.editor.useTypographyExtension]
27+
[appContext.prefs.editor.tabSize, appContext.prefs.editor.useTypographyExtension, appContext.prefs.editor.customTextStyles]
2728
);
2829

2930
const content = useMemo(() => JSON.parse(window.api.loadPage(page.fileName)), [page.fileName]);

packages/renderer/src/components/Views/Editor/TableOfContents.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
createStyles,
55
Paper,
66
rem,
7+
ScrollArea,
78
Space,
89
Text,
910
Tooltip,
@@ -27,7 +28,15 @@ const useStyles = createStyles((theme) => ({
2728
position: "absolute",
2829
top: "78px",
2930
zIndex: 3000,
30-
maxWidth: "300px"
31+
width: "250px",
32+
height: "400px",
33+
maxWidth: "600px",
34+
minWidth: "150px",
35+
minHeight: "150px",
36+
resize: "both",
37+
overflow: "hidden",
38+
display: "flex",
39+
flexDirection: "column"
3140
},
3241
link: {
3342
display: "block",
@@ -111,17 +120,19 @@ export function TableOfContents(props: { editor: Editor }) {
111120
<Text fz="xs">{texts.table_of_contents}</Text>
112121
<Space h="sm" />
113122

114-
{items.map((item: any, index: any) => (
115-
<Anchor
116-
key={index}
117-
className={classes.link}
118-
href={`#${item.id}`}
119-
pl={item.level * 16}
120-
truncate="end"
121-
>
122-
{item.text}
123-
</Anchor>
124-
))}
123+
<ScrollArea style={{ flexGrow: 1, paddingRight: "10px" }} type="auto" offsetScrollbars>
124+
{items.map((item: any, index: any) => (
125+
<Anchor
126+
key={index}
127+
className={classes.link}
128+
href={`#${item.id}`}
129+
pl={item.level * 16}
130+
truncate="end"
131+
>
132+
{item.text}
133+
</Anchor>
134+
))}
135+
</ScrollArea>
125136
</Paper>
126137
)}
127138
</Transition>

packages/renderer/src/components/Views/Editor/Toolbar/Toolbar.tsx

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -217,27 +217,7 @@ export default function Toolbar({ editor }: Props) {
217217
isActive={() => editor.isActive("blockQuote")}
218218
/>
219219

220-
<ToolbarSplit
221-
title={texts.heading}
222-
icon="heading"
223-
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
224-
>
225-
{[1, 2, 3, 4, 5, 6].map((i) => (
226-
<Menu.Item
227-
key={i}
228-
onClick={() =>
229-
editor
230-
.chain()
231-
.focus()
232-
.toggleHeading({ level: i as 1 | 2 | 3 | 4 | 5 | 6 })
233-
.run()
234-
}
235-
icon={<Icon icon={`h-${i}`} />}
236-
>
237-
{texts.heading_level} {i}
238-
</Menu.Item>
239-
))}
240-
</ToolbarSplit>
220+
241221

242222
<ToolbarButton
243223
title={texts.horizontal_rule}
@@ -519,6 +499,43 @@ export default function Toolbar({ editor }: Props) {
519499
</Button>
520500
</ToolbarDropdown>
521501

502+
<Select
503+
size="xs"
504+
w={140}
505+
icon={<Icon icon="brush" />}
506+
placeholder="Text Style"
507+
data={[
508+
{ value: "default", label: "Default" },
509+
...Object.keys(appContext.prefs.editor.customTextStyles || {})
510+
]}
511+
value={
512+
editor.getAttributes("textStyle").customStyleName ||
513+
editor.getAttributes("paragraph").customStyleName ||
514+
editor.getAttributes("heading").customStyleName ||
515+
"default"
516+
}
517+
onChange={(value) => {
518+
let chain = editor.chain().focus();
519+
if (value === "default" || value == null) {
520+
chain = chain.setParagraph().unsetCustomStyle();
521+
} else {
522+
const styleObj = appContext.prefs.editor.customTextStyles[value];
523+
if (styleObj && styleObj.tag) {
524+
if (styleObj.tag === "h1") chain = chain.setHeading({ level: 1 });
525+
else if (styleObj.tag === "h2") chain = chain.setHeading({ level: 2 });
526+
else if (styleObj.tag === "h3") chain = chain.setHeading({ level: 3 });
527+
else if (styleObj.tag === "h4") chain = chain.setHeading({ level: 4 });
528+
else if (styleObj.tag === "h5") chain = chain.setHeading({ level: 5 });
529+
else if (styleObj.tag === "h6") chain = chain.setHeading({ level: 6 });
530+
else if (styleObj.tag === "blockquote") chain = chain.setBlockquote();
531+
else if (styleObj.tag === "p") chain = chain.setParagraph();
532+
}
533+
chain = chain.setCustomStyle(value);
534+
}
535+
chain.run();
536+
}}
537+
/>
538+
522539
<Select
523540
size="xs"
524541
w={140}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Extension } from "@tiptap/core";
2+
import "@tiptap/extension-text-style";
3+
4+
export type CustomStyleOptions = {
5+
types: string[];
6+
customStyles: Record<string, Record<string, string>>;
7+
};
8+
9+
declare module "@tiptap/core" {
10+
interface Commands<ReturnType> {
11+
customStyle: {
12+
setCustomStyle: (styleName: string) => ReturnType;
13+
unsetCustomStyle: () => ReturnType;
14+
};
15+
}
16+
}
17+
18+
export const CustomStyle = Extension.create<CustomStyleOptions>({
19+
name: "customStyle",
20+
21+
addOptions() {
22+
return {
23+
types: ["textStyle", "paragraph", "heading"],
24+
customStyles: {}
25+
};
26+
},
27+
28+
addGlobalAttributes() {
29+
return [
30+
{
31+
types: this.options.types,
32+
attributes: {
33+
customStyleName: {
34+
default: null,
35+
parseHTML: (element) => element.getAttribute("data-custom-style"),
36+
renderHTML: (attributes) => {
37+
const stylesConfig = this.options.customStyles;
38+
if (!attributes.customStyleName || !stylesConfig[attributes.customStyleName]) {
39+
return {};
40+
}
41+
const cssObj = stylesConfig[attributes.customStyleName];
42+
const styleString = Object.keys(cssObj)
43+
.filter(v => v !== "tag" && v !== "type")
44+
.map((v) => `${v}: ${cssObj[v]}`)
45+
.join("; ");
46+
return {
47+
style: styleString,
48+
"data-custom-style": attributes.customStyleName
49+
};
50+
}
51+
}
52+
}
53+
}
54+
];
55+
},
56+
57+
addCommands() {
58+
return {
59+
setCustomStyle:
60+
(styleName) =>
61+
({ chain, state }) => {
62+
const { selection } = state;
63+
if (selection.empty) {
64+
return chain()
65+
.updateAttributes("paragraph", { customStyleName: styleName })
66+
.updateAttributes("heading", { customStyleName: styleName })
67+
.run();
68+
} else {
69+
return chain()
70+
.setMark("textStyle", { customStyleName: styleName })
71+
.run();
72+
}
73+
},
74+
unsetCustomStyle:
75+
() =>
76+
({ chain, state }) => {
77+
const { selection } = state;
78+
if (selection.empty) {
79+
return chain()
80+
.updateAttributes("paragraph", { customStyleName: null })
81+
.updateAttributes("heading", { customStyleName: null })
82+
.run();
83+
} else {
84+
return chain()
85+
.setMark("textStyle", { customStyleName: null })
86+
.removeEmptyTextStyle()
87+
.run();
88+
}
89+
}
90+
};
91+
}
92+
});

0 commit comments

Comments
 (0)