Skip to content

Commit 2bc44db

Browse files
authored
Merge pull request #7512 from nextcloud/backport/7509/stable31
[stable31] Fixes for TextDirection extension
2 parents e28935b + 2b477ee commit 2bc44db

File tree

5 files changed

+209
-17
lines changed

5 files changed

+209
-17
lines changed

src/css/prosemirror.scss

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ div.ProseMirror {
5050
display: flex;
5151
align-items: start;
5252
// Leave space for checkbox (14px width + 2x 1px border + 6px margin-right)
53-
margin-left: -24px;
53+
margin-inline-start: -24px;
5454

5555
input[type=checkbox] {
5656
display: none;
@@ -256,7 +256,7 @@ div.ProseMirror {
256256

257257
pre.frontmatter {
258258
margin-bottom: 2em;
259-
border-left: 4px solid var(--color-primary-element);
259+
border-inline-start: 4px solid var(--color-primary-element);
260260
}
261261

262262
pre.frontmatter::before {
@@ -274,17 +274,25 @@ div.ProseMirror {
274274

275275
li {
276276
position: relative;
277-
padding-left: 3px;
277+
padding-inline-start: 3px;
278278

279279
p {
280280
position: relative;
281281
margin-bottom: 0.5em;
282282
}
283283
}
284284

285+
li [dir="rtl"] {
286+
text-align: right;
287+
}
288+
289+
li [dir="ltr"] {
290+
text-align: left;
291+
}
292+
285293
ul, ol {
286-
padding-left: 10px;
287-
margin-left: 10px;
294+
padding-inline-start: 10px;
295+
margin-inline-start: 10px;
288296
margin-bottom: 1em;
289297
}
290298

@@ -303,11 +311,10 @@ div.ProseMirror {
303311
}
304312

305313
blockquote {
306-
padding-left: 1em;
307-
border-left: 4px solid var(--color-primary-element);
314+
padding-inline-start: 1em;
315+
border-inline-start: 4px solid var(--color-primary-element);
308316
color: var(--color-text-maxcontrast);
309-
margin-left: 0;
310-
margin-right: 0;
317+
margin-inline: 0;
311318
}
312319

313320
// table variables

src/extensions/RichText.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import Table from './../nodes/Table.js'
4141
import TaskItem from './../nodes/TaskItem.js'
4242
import TaskList from './../nodes/TaskList.js'
4343
import Text from '@tiptap/extension-text'
44+
import TextDirection from './../extensions/TextDirection.ts'
4445
import TrailingNode from './../nodes/TrailingNode.js'
4546
/* eslint-enable import/no-named-as-default */
4647

@@ -113,6 +114,15 @@ export default Extension.create({
113114
}),
114115
LinkBubble,
115116
TrailingNode,
117+
TextDirection.configure({
118+
types: [
119+
'heading',
120+
'paragraph',
121+
'listItem',
122+
'taskItem',
123+
'blockquote',
124+
],
125+
}),
116126
]
117127
const additionalExtensionNames = this.options.extensions.map(e => e.name)
118128
return [

src/extensions/TextDirection.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2023 Amir Hossein Hashemi
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
import { Extension } from '@tiptap/core'
7+
import { Plugin, PluginKey } from '@tiptap/pm/state'
8+
9+
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'
10+
const LTR = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6'
11+
+ '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C'
12+
+ '\uFE00-\uFE6F\uFEFD-\uFFFF'
13+
14+
/* eslint-disable no-misleading-character-class */
15+
const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
16+
const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
17+
18+
/**
19+
* @param text Text string
20+
*
21+
*Source: https://github.com/facebook/lexical/blob/429e3eb5b5a244026fa4776650aabe3c8e17536b/packages/lexical/src/LexicalUtils.ts#L163
22+
*/
23+
export function getTextDirection(text: string): 'ltr' | 'rtl' | null {
24+
if (text.length === 0) {
25+
return null
26+
}
27+
if (RTL_REGEX.test(text)) {
28+
return 'rtl'
29+
}
30+
if (LTR_REGEX.test(text)) {
31+
return 'ltr'
32+
}
33+
return null
34+
}
35+
36+
const validDirections = ['ltr', 'rtl', 'auto'] as const
37+
38+
type Direction = (typeof validDirections)[number]
39+
40+
/**
41+
* @param object Property object
42+
* @param object.types List of node types to consider
43+
*/
44+
function TextDirectionPlugin({ types }: { types: string[] }) {
45+
return new Plugin({
46+
key: new PluginKey('textDirection'),
47+
appendTransaction: (transactions, oldState, newState) => {
48+
const docChanges = transactions.some(
49+
(transaction) => transaction.docChanged,
50+
)
51+
if (!docChanges) {
52+
return
53+
}
54+
55+
let modified = false
56+
const tr = newState.tr
57+
tr.setMeta('addToHistory', false)
58+
59+
newState.doc.descendants((node, pos) => {
60+
if (types.includes(node.type.name)) {
61+
if (node.attrs.dir !== null && node.textContent.length > 0) {
62+
return
63+
}
64+
const newTextDirection = getTextDirection(node.textContent)
65+
if (node.attrs.dir === newTextDirection) {
66+
return
67+
}
68+
69+
const marks = tr.storedMarks || []
70+
tr.setNodeAttribute(pos, 'dir', newTextDirection)
71+
// `tr.setNodeAttribute` resets the stored marks so we'll restore them
72+
for (const mark of marks) {
73+
tr.addStoredMark(mark)
74+
}
75+
modified = true
76+
}
77+
})
78+
79+
return modified ? tr : null
80+
},
81+
})
82+
}
83+
84+
declare module '@tiptap/core' {
85+
interface Commands<ReturnType> {
86+
textDirection: {
87+
/**
88+
* Set the text direction attribute
89+
*/
90+
setTextDirection: (direction: Direction) => ReturnType
91+
/**
92+
* Unset the text direction attribute
93+
*/
94+
unsetTextDirection: () => ReturnType
95+
}
96+
}
97+
}
98+
99+
export interface TextDirectionOptions {
100+
types: string[]
101+
defaultDirection: Direction | null
102+
}
103+
104+
export const TextDirection = Extension.create<TextDirectionOptions>({
105+
name: 'textDirection',
106+
107+
addOptions() {
108+
return {
109+
types: [],
110+
defaultDirection: null,
111+
}
112+
},
113+
114+
addGlobalAttributes() {
115+
return [
116+
{
117+
types: this.options.types,
118+
attributes: {
119+
dir: {
120+
default: null,
121+
parseHTML: (element) =>
122+
element.dir || this.options.defaultDirection,
123+
renderHTML: (attributes) => {
124+
if (attributes.dir === this.options.defaultDirection) {
125+
return {}
126+
}
127+
return { dir: attributes.dir }
128+
},
129+
},
130+
},
131+
},
132+
]
133+
},
134+
135+
addCommands() {
136+
return {
137+
setTextDirection:
138+
(direction: Direction) =>
139+
({ commands }) => {
140+
if (!validDirections.includes(direction)) {
141+
return false
142+
}
143+
144+
return this.options.types.every((type) =>
145+
commands.updateAttributes(type, { dir: direction }),
146+
)
147+
},
148+
149+
unsetTextDirection:
150+
() =>
151+
({ commands }) => {
152+
return this.options.types.every((type) =>
153+
commands.resetAttributes(type, 'dir'),
154+
)
155+
},
156+
}
157+
},
158+
159+
addKeyboardShortcuts() {
160+
return {
161+
'Mod-Alt-l': () => this.editor.commands.setTextDirection('ltr'),
162+
'Mod-Alt-r': () => this.editor.commands.setTextDirection('rtl'),
163+
}
164+
},
165+
166+
addProseMirrorPlugins() {
167+
return [
168+
TextDirectionPlugin({
169+
types: this.options.types,
170+
}),
171+
]
172+
},
173+
})
174+
175+
export default TextDirection

src/nodes/Callout.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ export default {
5050
<style lang="scss" scoped>
5151
.callout {
5252
background-color: var(--callout-background, var(--color-background-hover));
53-
border-left-color: var(--callout-border, var(--color-primary-element));
53+
border-inline-start-color: var(--callout-border, var(--color-primary-element));
5454
border-radius: var(--border-radius);
5555
padding: 1em;
56-
padding-left: 0.5em;
57-
border-left-width: 0.3em;
58-
border-left-style: solid;
56+
padding-inline-start: 0.5em;
57+
border-inline-start-width: 0.3em;
58+
border-inline-start-style: solid;
5959
position: relative;
6060
margin-bottom: 0.5em;
6161
@@ -68,7 +68,7 @@ export default {
6868
}
6969
7070
.callout__content {
71-
margin-left: 1em;
71+
margin-inline-start: 1em;
7272
&:deep(p) {
7373
&:last-child {
7474
margin-bottom: 0;

src/tests/tiptap.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ const renderedHTML = (markdown) => {
1616
describe('TipTap', () => {
1717
it('render softbreaks', () => {
1818
const markdown = 'This\nis\none\nparagraph'
19-
expect(renderedHTML(markdown)).toEqual(`<p>${markdown}</p>`)
19+
expect(renderedHTML(markdown)).toEqual(`<p dir="ltr">${markdown}</p>`)
2020
})
2121

2222
it('render hardbreak', () => {
2323
const markdown = 'Hard line break \nNext Paragraph'
24-
expect(renderedHTML(markdown)).toEqual('<p>Hard line break<br>Next Paragraph</p>')
24+
expect(renderedHTML(markdown)).toEqual('<p dir="ltr">Hard line break<br>Next Paragraph</p>')
2525
})
2626

2727
it('render taskList', () => {
2828
const markdown = '* [ ] item 1\n'
29-
expect(renderedHTML(markdown)).toEqual('<ul class="contains-task-list"><li data-checked="false" class="task-list-item checkbox-item"><input type="checkbox" class="" contenteditable="false"><label><p>item 1</p></label></li></ul>')
29+
expect(renderedHTML(markdown)).toEqual('<ul class="contains-task-list"><li dir="ltr" data-checked="false" class="task-list-item checkbox-item"><input type="checkbox" class="" contenteditable="false"><label><p dir="ltr">item 1</p></label></li></ul>')
3030
})
3131
})

0 commit comments

Comments
 (0)