Skip to content

Commit 178d2a7

Browse files
authored
Reuse tiptap ordered list type (#103)
1 parent 4143d80 commit 178d2a7

7 files changed

Lines changed: 253 additions & 205 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ const isNodeType = (editor, typeName) => {
7979

8080
Commit without the `Co-Authored-By` attribution line (no `--co-author` / no `Co-Authored-By: Claude` trailer).
8181

82+
Committing feature by feature (one focused commit per logical change, rather than one large batched commit) is appreciated.
83+
8284
## Testing Workflow
8385

8486
1. Make code changes

django_prose_editor/static/django_prose_editor/editor.js

Lines changed: 15 additions & 15 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.

package.json

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,37 @@
55
"license": "MIT",
66
"dependencies": {
77
"@rslib/core": "^0.23.2",
8-
"@tiptap/core": "^3.30.2",
9-
"@tiptap/extension-blockquote": "^3.30.2",
10-
"@tiptap/extension-bold": "^3.30.2",
11-
"@tiptap/extension-bubble-menu": "^3.30.2",
12-
"@tiptap/extension-code": "^3.30.2",
13-
"@tiptap/extension-code-block": "^3.30.2",
14-
"@tiptap/extension-color": "^3.30.2",
15-
"@tiptap/extension-document": "^3.30.2",
16-
"@tiptap/extension-hard-break": "^3.30.2",
17-
"@tiptap/extension-heading": "^3.30.2",
18-
"@tiptap/extension-highlight": "^3.30.2",
19-
"@tiptap/extension-horizontal-rule": "^3.30.2",
20-
"@tiptap/extension-image": "^3.30.2",
21-
"@tiptap/extension-invisible-characters": "^3.30.2",
22-
"@tiptap/extension-italic": "^3.30.2",
23-
"@tiptap/extension-link": "^3.30.2",
24-
"@tiptap/extension-list": "^3.30.2",
25-
"@tiptap/extension-paragraph": "^3.30.2",
26-
"@tiptap/extension-strike": "^3.30.2",
27-
"@tiptap/extension-subscript": "^3.30.2",
28-
"@tiptap/extension-superscript": "^3.30.2",
29-
"@tiptap/extension-table": "^3.30.2",
30-
"@tiptap/extension-text": "^3.30.2",
31-
"@tiptap/extension-text-align": "^3.30.2",
32-
"@tiptap/extension-text-style": "^3.30.2",
33-
"@tiptap/extension-underline": "^3.30.2",
34-
"@tiptap/extensions": "^3.30.2",
35-
"@tiptap/pm": "^3.30.2",
8+
"@tiptap/core": "^3.30.3",
9+
"@tiptap/extension-blockquote": "^3.30.3",
10+
"@tiptap/extension-bold": "^3.30.3",
11+
"@tiptap/extension-bubble-menu": "^3.30.3",
12+
"@tiptap/extension-code": "^3.30.3",
13+
"@tiptap/extension-code-block": "^3.30.3",
14+
"@tiptap/extension-color": "^3.30.3",
15+
"@tiptap/extension-document": "^3.30.3",
16+
"@tiptap/extension-hard-break": "^3.30.3",
17+
"@tiptap/extension-heading": "^3.30.3",
18+
"@tiptap/extension-highlight": "^3.30.3",
19+
"@tiptap/extension-horizontal-rule": "^3.30.3",
20+
"@tiptap/extension-image": "^3.30.3",
21+
"@tiptap/extension-invisible-characters": "^3.30.3",
22+
"@tiptap/extension-italic": "^3.30.3",
23+
"@tiptap/extension-link": "^3.30.3",
24+
"@tiptap/extension-list": "^3.30.3",
25+
"@tiptap/extension-paragraph": "^3.30.3",
26+
"@tiptap/extension-strike": "^3.30.3",
27+
"@tiptap/extension-subscript": "^3.30.3",
28+
"@tiptap/extension-superscript": "^3.30.3",
29+
"@tiptap/extension-table": "^3.30.3",
30+
"@tiptap/extension-text": "^3.30.3",
31+
"@tiptap/extension-text-align": "^3.30.3",
32+
"@tiptap/extension-text-style": "^3.30.3",
33+
"@tiptap/extension-underline": "^3.30.3",
34+
"@tiptap/extensions": "^3.30.3",
35+
"@tiptap/pm": "^3.30.3",
3636
"autoprefixer": "^10.5.4",
3737
"postcss": "^8.5.26",
38-
"prosemirror-changeset": "^2.4.1",
38+
"prosemirror-changeset": "^2.4.2",
3939
"prosemirror-commands": "^1.7.2",
4040
"prosemirror-dropcursor": "^1.8.3",
4141
"prosemirror-gapcursor": "^1.4.1",

src/orderedList.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,28 +105,32 @@ export const OrderedList = TiptapOrderedList.configure({
105105

106106
addAttributes() {
107107
const listTypes = this.options.listTypes
108+
const valid_types = listTypes.map(({ type }) => type)
109+
const parentAttributes = this.parent?.() ?? {}
110+
// Tiptap's own parseHTML (as of 3.27) already sniffs the HTML `type`
111+
// attribute as well as `list-style-type` CSS on the <ol> and on its
112+
// first <li> (the pattern Google Docs pastes use). Reuse it as a
113+
// fallback instead of reimplementing that detection ourselves.
114+
const parentParseHTML = parentAttributes.type?.parseHTML
108115

109116
return {
110-
...this.parent?.(),
111-
// TODO we could try removing this; we still would need the data-type to
112-
// be rendered, but the parsing could be reused now that Tiptap 3.27 has
113-
// added support for the type attribute as well.
117+
...parentAttributes,
114118
type: {
115119
default: null,
116120
parseHTML: (element) => {
117-
const typeAttribute = element.getAttribute("type"),
118-
dataType = element.dataset.type?.replace("latin", "alpha"),
119-
valid_types = listTypes.map(({ type }) => type)
120-
121+
// Our own data-type is authoritative when present: it is the only
122+
// encoding that survives round-tripping through our renderHTML,
123+
// and unlike the HTML `type` attribute it isn't ambiguous under
124+
// case-insensitive attribute matching in some browsers (e.g.
125+
// between `type="a"` and `type="A"`).
126+
const dataType = element.dataset.type?.replace("latin", "alpha")
121127
if (dataType && valid_types.includes(dataType)) {
122128
return dataType
123129
}
124130

125-
if (
126-
typeAttribute &&
127-
valid_types.includes(htmlToCssMap[typeAttribute])
128-
) {
129-
return htmlToCssMap[typeAttribute]
131+
const htmlType = parentParseHTML?.(element)
132+
if (htmlType && valid_types.includes(htmlToCssMap[htmlType])) {
133+
return htmlToCssMap[htmlType]
130134
}
131135

132136
return valid_types[0]

tests/testapp/test_prose_editor_e2e.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,48 @@ def test_prose_editor_ordered_list_without_type_attribute(page, live_server):
300300
assert not console_errors, f"Unexpected console errors: {console_errors}"
301301

302302

303+
@pytest.mark.django_db
304+
@pytest.mark.e2e
305+
def test_prose_editor_ordered_list_html_type_attribute_only(page, live_server):
306+
"""An <ol type="..."> without data-type (e.g. hand-written HTML) is normalized."""
307+
_login(page, live_server)
308+
309+
model = TableProseEditorModel.objects.create(
310+
description='<ol type="A"><li>First item</li></ol>'
311+
)
312+
313+
page.goto(
314+
f"{live_server.url}/admin/testapp/tableproseeditormodel/{model.id}/change/"
315+
)
316+
317+
editor = page.locator(".prose-editor > .ProseMirror")
318+
ol_element = editor.locator("ol")
319+
expect(ol_element).to_have_attribute("data-type", "upper-alpha")
320+
expect(ol_element).to_have_attribute("type", "A")
321+
322+
323+
@pytest.mark.django_db
324+
@pytest.mark.e2e
325+
def test_prose_editor_ordered_list_css_list_style_type_paste(page, live_server):
326+
"""An <ol> with only CSS list-style-type (typical of Word/Google Docs pastes)
327+
is normalized using Tiptap's own upstream parsing, without a data-type attribute."""
328+
_login(page, live_server)
329+
330+
model = TableProseEditorModel.objects.create(
331+
description=(
332+
'<ol style="list-style-type: upper-roman"><li>First item</li></ol>'
333+
)
334+
)
335+
336+
page.goto(
337+
f"{live_server.url}/admin/testapp/tableproseeditormodel/{model.id}/change/"
338+
)
339+
340+
editor = page.locator(".prose-editor > .ProseMirror")
341+
ol_element = editor.locator("ol")
342+
expect(ol_element).to_have_attribute("data-type", "upper-roman")
343+
344+
303345
@pytest.mark.django_db
304346
@pytest.mark.e2e
305347
def test_configurable_prose_editor_admin(page, live_server):

0 commit comments

Comments
 (0)