Skip to content

Commit e2c2c67

Browse files
committed
First working version of NodeClass for marks
1 parent 6476f3e commit e2c2c67

File tree

7 files changed

+426
-107
lines changed

7 files changed

+426
-107
lines changed

AGENTS.md

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ The project uses:
2929

3030
JavaScript source files are in `src/` and get built into `django_prose_editor/static/`.
3131

32+
**IMPORTANT**: After modifying JavaScript files in `src/`, you MUST rebuild with:
33+
```bash
34+
yarn prod
35+
```
36+
37+
The tests run against the compiled JavaScript in `django_prose_editor/static/`, not the source files.
38+
3239
## Documentation
3340

3441
Documentation is written in ReStructuredText (`.rst`) format in the `docs/` directory.
@@ -69,10 +76,11 @@ const isNodeType = (editor, typeName) => {
6976
## Testing Workflow
7077

7178
1. Make code changes
72-
2. Run linting/formatting: `prek run --all-files` (or let it run on commit)
73-
3. Run tests: `tox -e py313-dj52`
74-
4. Verify all tests pass (32 tests expected)
75-
5. Update documentation if needed
79+
2. **If you modified JavaScript**: Run `yarn prod` to rebuild
80+
3. Run linting/formatting: `prek run --all-files` (or let it run on commit)
81+
4. Run tests: `tox -e py313-dj52`
82+
5. Verify all tests pass (35 tests expected as of 2025-11-04)
83+
6. Update documentation if needed
7684

7785
## Common Tasks
7886

@@ -93,3 +101,50 @@ Extensions are configured in two ways:
93101
- **JavaScript**: Via `Extension.configure({...})`
94102

95103
Keep both configuration methods documented and in sync.
104+
105+
## ProseMirror/Tiptap Patterns
106+
107+
### Modifying Mark Attributes
108+
109+
**Important**: Mark attributes cannot be modified in place in ProseMirror. You must use the unsetMark/setMark pattern:
110+
111+
```javascript
112+
// ❌ WRONG - This doesn't work
113+
tr.setMarkAttribute(from, to, markType, 'class', 'newValue')
114+
115+
// ✅ CORRECT - Unset and reapply with new attributes
116+
const currentMark = markType.isInSet($pos.marks())
117+
const newAttrs = { ...currentMark.attrs, class: 'newValue' }
118+
editor.chain()
119+
.extendMarkRange(typeName)
120+
.unsetMark(typeName)
121+
.setMark(typeName, newAttrs)
122+
.run()
123+
```
124+
125+
Always preserve existing attributes when modifying marks to avoid losing data like `href` on links, `src` on images, etc.
126+
127+
### Extending Mark Range
128+
129+
When working with marks at a collapsed selection (cursor position), use `extendMarkRange()` to select the entire mark:
130+
131+
```javascript
132+
editor.chain()
133+
.extendMarkRange('bold') // Selects entire bold region
134+
.setMark('bold', { class: 'emphasis' })
135+
.run()
136+
```
137+
138+
### Checking Active Marks
139+
140+
To check if a mark exists at the cursor position, use the resolved position's marks, not just `isActive()`:
141+
142+
```javascript
143+
const { state } = editor
144+
const { $from } = state.selection
145+
const markType = state.schema.marks[typeName]
146+
const marks = $from.marks()
147+
const hasMark = marks.some(mark => mark.type === markType)
148+
```
149+
150+
The `isActive()` method can be unreliable at mark boundaries or with collapsed selections.

django_prose_editor/static/django_prose_editor/editor.js

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

django_prose_editor/static/django_prose_editor/editor.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/nodeclass.rst

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
NodeClass Extension
22
===================
33

4-
The NodeClass extension allows you to apply arbitrary CSS classes to block-level nodes (paragraphs, tables, table cells, etc.) using global attributes. This provides a clean, semantic way to style entire elements without requiring individual node type extensions.
4+
The NodeClass extension allows you to apply arbitrary CSS classes to both block-level nodes (paragraphs, tables, table cells, etc.) and marks (bold, italic, links, etc.) using global attributes. This provides a clean, semantic way to style elements without requiring individual node or mark type extensions.
55

6-
Unlike TextClass which applies to inline text using ``<span>`` tags, NodeClass works with block-level elements by adding CSS classes directly to their HTML tags (e.g., ``<p class="highlight">``, ``<table class="bordered">``).
6+
Unlike TextClass which applies to inline text using ``<span>`` tags, NodeClass works with both block-level elements and marks by adding CSS classes directly to their HTML tags (e.g., ``<p class="highlight">``, ``<table class="bordered">``, ``<strong class="emphasis">``).
77

88
Basic Usage
99
-----------
1010

11-
To use the NodeClass extension, configure it with CSS classes organized by node type. Each class can be specified as:
11+
To use the NodeClass extension, configure it with CSS classes organized by node or mark type. Each class can be specified as:
1212

1313
- A string (class name and display title will be the same)
1414
- An object with ``className`` and ``title`` properties for custom display names
@@ -25,6 +25,7 @@ To use the NodeClass extension, configure it with CSS classes organized by node
2525
"Table": True,
2626
"NodeClass": {
2727
"cssClasses": {
28+
# Node types
2829
"paragraph": {
2930
"title": "Paragraph",
3031
"cssClasses": [
@@ -56,20 +57,20 @@ To use the NodeClass extension, configure it with CSS classes organized by node
5657
{"className": "accent", "title": "Accent Heading"}
5758
]
5859
},
59-
"bulletList": {
60-
"title": "List",
60+
# Mark types
61+
"bold": {
62+
"title": "Bold",
6163
"cssClasses": [
62-
"checklist",
63-
"no-bullets",
64-
{"className": "spaced", "title": "Spaced List"}
64+
"emphasis",
65+
{"className": "important", "title": "Important"}
6566
]
6667
},
67-
"orderedList": {
68-
"title": "Numbered List",
68+
"link": {
69+
"title": "Link",
6970
"cssClasses": [
70-
"alpha",
71-
"roman",
72-
{"className": "outline", "title": "Outline Style"}
71+
"external",
72+
"download",
73+
{"className": "button", "title": "Button Link"}
7374
]
7475
}
7576
}
@@ -89,11 +90,14 @@ For simpler use cases, you can still use the array format without custom titles:
8990
extensions={
9091
"NodeClass": {
9192
"cssClasses": {
93+
# Node types
9294
"paragraph": ["highlight", "callout", "centered"],
9395
"table": ["bordered", "striped", "compact"],
9496
"tableCell": ["centered", "right-aligned", "numeric"],
9597
"bulletList": ["checklist", "no-bullets", "spaced"],
96-
"orderedList": ["alpha", "roman", "outline"]
98+
# Mark types
99+
"bold": ["emphasis", "important"],
100+
"link": ["external", "download", "button"]
97101
}
98102
}
99103
}
@@ -108,13 +112,17 @@ When creating custom presets, you can configure the NodeClass extension in JavaS
108112
109113
import { NodeClass } from "django-prose-editor/editor"
110114
111-
// Per-node configuration
115+
// Configuration for nodes and marks
112116
NodeClass.configure({
113117
cssClasses: {
118+
// Node types
114119
paragraph: ["highlight", "callout", "centered"],
115120
table: ["bordered", "striped", "compact"],
116121
tableCell: ["centered", "right-aligned", "numeric"],
117-
heading: ["section-title", "accent"]
122+
heading: ["section-title", "accent"],
123+
// Mark types
124+
bold: ["emphasis", "important"],
125+
link: ["external", "download", "button"]
118126
}
119127
})
120128
@@ -128,14 +136,20 @@ When creating custom presets, you can configure the NodeClass extension in JavaS
128136
table: [
129137
{ className: "bordered", title: "Bordered Table" },
130138
{ className: "striped", title: "Striped Rows" }
139+
],
140+
bold: [
141+
{ className: "emphasis", title: "Emphasis" },
142+
{ className: "important", title: "Important" }
131143
]
132144
}
133145
})
134146
135-
Supported Node Types
136-
--------------------
147+
Supported Types
148+
---------------
149+
150+
The following node and mark types are supported for CSS class application:
137151

138-
The following node types are supported for CSS class application:
152+
**Node Types:**
139153

140154
- **paragraph**: Paragraph elements (``<p>``)
141155
- **table**: Table elements (``<table>``)
@@ -148,45 +162,58 @@ The following node types are supported for CSS class application:
148162
- **blockquote**: Blockquote elements (``<blockquote>``)
149163
- **codeBlock**: Code block elements (``<pre>``)
150164

165+
**Mark Types:**
166+
167+
- **bold**: Bold text (``<strong>``)
168+
- **italic**: Italic text (``<em>``)
169+
- **link**: Links (``<a>``)
170+
- **code**: Inline code (``<code>``)
171+
- **strike**: Strikethrough text (``<s>``)
172+
- **underline**: Underlined text (``<u>``)
173+
151174
Menu Integration
152175
----------------
153176

154-
When configured with CSS classes, NodeClass automatically adds context-sensitive dropdown menus to the editor. The menu options change based on the currently selected node type:
177+
When configured with CSS classes, NodeClass automatically adds context-sensitive dropdown menus to the editor. The menu options change based on the currently selected node or mark type:
155178

156179
- When a paragraph is selected, only paragraph classes are shown
157180
- When a table is selected, only table classes are shown
158-
- When a table cell is selected, only table cell classes are shown
181+
- When text with a bold mark is selected, bold classes are shown
182+
- When a link is selected, link classes are shown
159183

160184
Each dropdown includes:
161185

162-
- **default**: Removes any applied node class (returns to normal styling)
163-
- Each configured CSS class for that node type as a selectable option
186+
- **Reset classes**: Removes any applied classes from nodes and marks (returns to normal styling)
187+
- Each configured CSS class for the applicable types as a selectable option
164188

165-
The menu items appear in the ``nodeClass`` group and are contextually filtered.
189+
The menu items appear in the ``nodeClass`` group and are contextually filtered. Mark class options are hidden when the selection is empty or when the mark type is not active in the current selection.
166190

167191
Commands
168192
--------
169193

170-
The NodeClass extension provides these commands:
194+
The NodeClass extension works automatically through menu integration. For marks, classes are applied using the standard mark commands:
171195

172196
.. code-block:: javascript
173197
174-
// Apply a CSS class to the current node
175-
editor.commands.setNodeClass("highlight")
198+
// For marks: Apply a mark with a specific class
199+
editor.commands.setMark("bold", { class: "emphasis" })
200+
editor.commands.setMark("link", { class: "external" })
176201
177-
// Remove node class from the current node
178-
editor.commands.unsetNodeClass()
202+
// Check if a mark with a specific class is active
203+
editor.isActive("bold", { class: "emphasis" })
204+
editor.isActive("link", { class: "external" })
179205
180-
// Check if current node has a specific class applied
181-
editor.isActive("nodeClass", { class: "highlight" })
206+
// Remove marks (which removes their classes)
207+
editor.commands.unsetMark("bold")
182208
183209
HTML Output
184210
-----------
185211

186-
The extension adds CSS classes directly to block-level elements:
212+
The extension adds CSS classes directly to both block-level elements and marks:
187213

188214
.. code-block:: html
189215

216+
<!-- Node classes -->
190217
<p class="highlight">This paragraph has highlighting applied.</p>
191218

192219
<table class="bordered striped">
@@ -202,15 +229,22 @@ The extension adds CSS classes directly to block-level elements:
202229
<p>Important quote or callout text.</p>
203230
</blockquote>
204231

232+
<!-- Mark classes -->
233+
<p>This is <strong class="emphasis">emphasized bold text</strong>.</p>
234+
235+
<p>Visit our <a href="https://example.com" class="external">website</a>.</p>
236+
237+
<p>This is <strong class="important">very important</strong> information.</p>
238+
205239
Sanitization
206240
------------
207241

208-
When using server-side sanitization, the NodeClass extension automatically configures the sanitizer to allow ``class`` attributes on all supported block-level elements.
242+
When using server-side sanitization, the NodeClass extension automatically configures the sanitizer to allow ``class`` attributes on all supported block-level elements and marks.
209243

210244
Styling Examples
211245
----------------
212246

213-
Define CSS rules in your stylesheet to style the configured classes:
247+
Define CSS rules in your stylesheet to style the configured classes for both nodes and marks:
214248

215249
.. code-block:: css
216250
@@ -288,6 +322,36 @@ Define CSS rules in your stylesheet to style the configured classes:
288322
padding-left: 1rem;
289323
}
290324
325+
/* Mark classes */
326+
.ProseMirror strong.emphasis {
327+
color: #d32f2f;
328+
font-weight: 700;
329+
}
330+
331+
.ProseMirror strong.important {
332+
background-color: #fff176;
333+
padding: 0 0.25rem;
334+
font-weight: 900;
335+
}
336+
337+
.ProseMirror a.external::after {
338+
content: "";
339+
font-size: 0.8em;
340+
}
341+
342+
.ProseMirror a.button {
343+
display: inline-block;
344+
padding: 0.5rem 1rem;
345+
background-color: #2196f3;
346+
color: white;
347+
text-decoration: none;
348+
border-radius: 4px;
349+
}
350+
351+
.ProseMirror a.button:hover {
352+
background-color: #1976d2;
353+
}
354+
291355
/* List classes */
292356
.ProseMirror ul.checklist {
293357
list-style: none;
@@ -404,7 +468,12 @@ Comparison with TextClass
404468

405469
NodeClass complements TextClass by targeting different content levels:
406470

407-
- **TextClass**: Applies to inline text spans within content (``<span class="...">``)
408-
- **NodeClass**: Applies to entire block-level elements (``<p class="...">``, ``<table class="...">``)
471+
- **TextClass**: Applies to inline text spans using ``<span>`` tags (``<span class="...">``)
472+
- **NodeClass**: Applies to both:
473+
474+
- Entire block-level elements (``<p class="...">``, ``<table class="...">``)
475+
- Inline marks using their native tags (``<strong class="...">``, ``<a class="...">``)
476+
477+
Use TextClass when you need a generic ``<span>`` wrapper for styling arbitrary text, and NodeClass when you want to style specific nodes or marks with their semantic HTML tags. They can be used together for comprehensive styling control.
409478

410-
Use TextClass for styling words or phrases within paragraphs, and NodeClass for styling entire structural elements. They can be used together for comprehensive styling control.
479+
**Note**: If you use the same name for both a node type and a mark type in ``cssClasses``, only the node type configuration will be recognized by the NodeClass extension.

0 commit comments

Comments
 (0)