Skip to content

Commit 35a9c53

Browse files
authored
Merge pull request #7509 from nextcloud/fix/text_direction
Fixes for TextDirection extension
2 parents 83dfe0c + 30aa6ad commit 35a9c53

File tree

7 files changed

+198
-35
lines changed

7 files changed

+198
-35
lines changed

package-lock.json

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

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@
9696
"proxy-polyfill": "^0.3.2",
9797
"slug": "^11.0.0",
9898
"tippy.js": "^6.3.7",
99-
"tiptap-text-direction": "^0.3.2",
10099
"uuid": "^11.1.0",
101100
"vue": "^2.7.16",
102101
"vue-click-outside": "^1.1.0",

src/css/prosemirror.scss

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

5656
input[type='checkbox'] {
5757
display: none;
@@ -262,7 +262,7 @@ div.ProseMirror {
262262

263263
pre.frontmatter {
264264
margin-bottom: 2em;
265-
border-left: 4px solid var(--color-primary-element);
265+
border-inline-start: 4px solid var(--color-primary-element);
266266
}
267267

268268
pre.frontmatter::before {
@@ -280,7 +280,7 @@ div.ProseMirror {
280280

281281
li {
282282
position: relative;
283-
padding-left: 3px;
283+
padding-inline-start: 3px;
284284

285285
p {
286286
position: relative;
@@ -298,8 +298,8 @@ div.ProseMirror {
298298

299299
ul,
300300
ol {
301-
padding-left: 10px;
302-
margin-left: 10px;
301+
padding-inline-start: 10px;
302+
margin-inline-start: 10px;
303303
margin-bottom: 1em;
304304
}
305305

@@ -318,11 +318,10 @@ div.ProseMirror {
318318
}
319319

320320
blockquote {
321-
padding-left: 1em;
322-
border-left: 4px solid var(--color-primary-element);
321+
padding-inline-start: 1em;
322+
border-inline-start: 4px solid var(--color-primary-element);
323323
color: var(--color-text-maxcontrast);
324-
margin-left: 0;
325-
margin-right: 0;
324+
margin-inline: 0;
326325
}
327326

328327
// table variables

src/extensions/RichText.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import Gapcursor from '@tiptap/extension-gapcursor'
1616
import HorizontalRule from '@tiptap/extension-horizontal-rule'
1717
import ListItem from '@tiptap/extension-list-item'
1818
import Text from '@tiptap/extension-text'
19-
import TextDirection from 'tiptap-text-direction'
2019
import MentionSuggestion from '../components/Suggestion/Mention/suggestions.js'
2120
import Heading from '../nodes/Heading.js'
2221
import EmojiSuggestion from './../components/Suggestion/Emoji/suggestions.js'
@@ -25,6 +24,7 @@ import LinkPicker from './../extensions/LinkPicker.js'
2524
import Markdown from './../extensions/Markdown.js'
2625
import Mention from './../extensions/Mention.js'
2726
import Search from './../extensions/Search.js'
27+
import TextDirection from './../extensions/TextDirection.ts'
2828
import BulletList from './../nodes/BulletList.js'
2929
import Callouts from './../nodes/Callouts.js'
3030
import CodeBlock from './../nodes/CodeBlock.js'
@@ -120,7 +120,13 @@ export default Extension.create({
120120
LinkBubble,
121121
TrailingNode,
122122
TextDirection.configure({
123-
types: ['heading', 'paragraph', 'listItem', 'orderedList'],
123+
types: [
124+
'heading',
125+
'paragraph',
126+
'listItem',
127+
'taskItem',
128+
'blockquote',
129+
],
124130
}),
125131
]
126132
const additionalExtensionNames = this.options.extensions.map((e) => e.name)

src/extensions/TextDirection.ts

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

src/nodes/Callout.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ export default {
5151
<style lang="scss" scoped>
5252
.callout {
5353
background-color: var(--callout-background, var(--color-background-hover));
54-
border-left-color: var(--callout-border, var(--color-primary-element));
54+
border-inline-start-color: var(--callout-border, var(--color-primary-element));
5555
border-radius: var(--border-radius);
5656
padding: 1em;
57-
padding-left: 0.5em;
58-
border-left-width: 0.3em;
59-
border-left-style: solid;
57+
padding-inline-start: 0.5em;
58+
border-inline-start-width: 0.3em;
59+
border-inline-start-style: solid;
6060
position: relative;
6161
margin-bottom: 0.5em;
6262
@@ -69,7 +69,7 @@ export default {
6969
}
7070
7171
.callout__content {
72-
margin-left: 1em;
72+
margin-inline-start: 1em;
7373
&:deep(p) {
7474
&:last-child {
7575
margin-bottom: 0;

src/tests/tiptap.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('TipTap', () => {
3131
it('render taskList', () => {
3232
const markdown = '* [ ] item 1\n'
3333
expect(renderedHTML(markdown)).toEqual(
34-
'<ul class="contains-task-list"><li 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>',
34+
'<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>',
3535
)
3636
})
3737
})

0 commit comments

Comments
 (0)