Skip to content

Commit 5bb72db

Browse files
committed
fix: mixed alignment across paragraphs
1 parent ff02e3e commit 5bb72db

5 files changed

Lines changed: 209 additions & 210 deletions

File tree

packages/super-editor/src/editors/v1/extensions/text-align/text-align.js

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,34 +41,61 @@ export const TextAlign = Extension.create({
4141
*/
4242
setTextAlign:
4343
(alignment) =>
44-
({ commands, state }) => {
44+
({ commands, state, tr, dispatch }) => {
4545
const containsAlignment = this.options.alignments.includes(alignment);
4646
if (!containsAlignment) return false;
47-
const $from = state?.selection?.$from;
48-
let paragraphNode = null;
49-
let paragraphDepth = -1;
50-
51-
if ($from) {
52-
for (let depth = $from.depth; depth >= 0; depth--) {
53-
const nodeAtDepth = $from.node(depth);
54-
if (nodeAtDepth?.type?.name === 'paragraph') {
55-
paragraphNode = nodeAtDepth;
56-
paragraphDepth = depth;
57-
break;
58-
}
59-
}
47+
48+
if (!state?.doc || !state?.selection || !tr) {
49+
const paragraphProperties = getSelectionParagraphProperties(this.editor, state);
50+
const storedAlignment = mapDisplayAlignmentToStoredJustification(
51+
alignment,
52+
paragraphProperties?.rightToLeft,
53+
);
54+
return commands.updateAttributes('paragraph', { 'paragraphProperties.justification': storedAlignment });
6055
}
6156

62-
let paragraphProperties = paragraphNode?.attrs?.paragraphProperties ?? {};
57+
const visitedPositions = new Set();
58+
let touched = false;
6359

64-
if (this.editor && $from && paragraphNode && paragraphDepth > 0) {
65-
const paragraphStartPos = $from.before(paragraphDepth);
66-
const paragraphPos = state.doc.resolve(paragraphStartPos);
67-
paragraphProperties = calculateResolvedParagraphProperties(this.editor, paragraphNode, paragraphPos);
68-
}
60+
const updateParagraph = (node, pos) => {
61+
if (node.type.name !== 'paragraph') return true;
62+
if (visitedPositions.has(pos)) return false;
63+
visitedPositions.add(pos);
64+
65+
const paragraphProperties = this.editor
66+
? calculateResolvedParagraphProperties(this.editor, node, state.doc.resolve(pos))
67+
: (node.attrs?.paragraphProperties ?? {});
68+
const storedAlignment = mapDisplayAlignmentToStoredJustification(
69+
alignment,
70+
paragraphProperties?.rightToLeft,
71+
);
72+
const existingParagraphProperties = node.attrs?.paragraphProperties ?? {};
73+
74+
if (existingParagraphProperties.justification === storedAlignment) return false;
75+
76+
tr.setNodeMarkup(pos, undefined, {
77+
...node.attrs,
78+
paragraphProperties: {
79+
...existingParagraphProperties,
80+
justification: storedAlignment,
81+
},
82+
});
83+
touched = true;
84+
return false;
85+
};
6986

70-
const storedAlignment = mapDisplayAlignmentToStoredJustification(alignment, paragraphProperties?.rightToLeft);
71-
return commands.updateAttributes('paragraph', { 'paragraphProperties.justification': storedAlignment });
87+
state.selection.ranges.forEach((range) => {
88+
if (range.$from.pos === range.$to.pos) {
89+
const paragraph = getParagraphAtSelection(range.$from);
90+
if (paragraph) updateParagraph(paragraph.node, paragraph.pos);
91+
return;
92+
}
93+
94+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, updateParagraph);
95+
});
96+
97+
if (touched && dispatch) dispatch(tr);
98+
return true;
7299
},
73100

74101
/**
@@ -94,3 +121,21 @@ export const TextAlign = Extension.create({
94121
};
95122
},
96123
});
124+
125+
function getSelectionParagraphProperties(editor, state) {
126+
const paragraph = getParagraphAtSelection(state?.selection?.$from);
127+
if (!paragraph) return {};
128+
if (!editor || !state?.doc) return paragraph.node?.attrs?.paragraphProperties ?? {};
129+
return calculateResolvedParagraphProperties(editor, paragraph.node, state.doc.resolve(paragraph.pos));
130+
}
131+
132+
function getParagraphAtSelection($pos) {
133+
if (!$pos) return null;
134+
for (let depth = $pos.depth; depth >= 0; depth--) {
135+
const node = $pos.node(depth);
136+
if (node?.type?.name === 'paragraph') {
137+
return { node, pos: depth > 0 ? $pos.before(depth) : 0 };
138+
}
139+
}
140+
return null;
141+
}

packages/super-editor/src/editors/v1/extensions/text-align/text-align.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,52 @@ describe('TextAlign extension', () => {
144144
});
145145
});
146146

147+
it('maps alignment per paragraph for mixed LTR/RTL selections', () => {
148+
const ltrParagraph = {
149+
type: { name: 'paragraph' },
150+
attrs: { paragraphProperties: { rightToLeft: false, justification: 'center' } },
151+
};
152+
const rtlParagraph = {
153+
type: { name: 'paragraph' },
154+
attrs: { paragraphProperties: { rightToLeft: true, justification: 'center' } },
155+
};
156+
const state = {
157+
doc: {
158+
resolve: vi.fn((pos) => ({ pos })),
159+
nodesBetween: vi.fn((_from, _to, callback) => {
160+
callback(ltrParagraph, 1);
161+
callback(rtlParagraph, 10);
162+
}),
163+
},
164+
selection: {
165+
ranges: [
166+
{
167+
$from: { pos: 1 },
168+
$to: { pos: 20 },
169+
},
170+
],
171+
},
172+
};
173+
const tr = { setNodeMarkup: vi.fn() };
174+
const dispatch = vi.fn();
175+
176+
const result = commands.setTextAlign('left')({
177+
commands: { updateAttributes: vi.fn(() => true) },
178+
state,
179+
tr,
180+
dispatch,
181+
});
182+
183+
expect(result).toBe(true);
184+
expect(tr.setNodeMarkup).toHaveBeenNthCalledWith(1, 1, undefined, {
185+
paragraphProperties: { rightToLeft: false, justification: 'left' },
186+
});
187+
expect(tr.setNodeMarkup).toHaveBeenNthCalledWith(2, 10, undefined, {
188+
paragraphProperties: { rightToLeft: true, justification: 'right' },
189+
});
190+
expect(dispatch).toHaveBeenCalledWith(tr);
191+
});
192+
147193
it('returns false for unsupported alignment values', () => {
148194
const updateAttributes = vi.fn(() => true);
149195

0 commit comments

Comments
 (0)