diff --git a/package-lock.json b/package-lock.json index 76c62d168ad..4363b320d19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,6 @@ "proxy-polyfill": "^0.3.2", "slug": "^11.0.0", "tippy.js": "^6.3.7", - "tiptap-text-direction": "^0.3.2", "uuid": "^11.1.0", "vue": "^2.7.16", "vue-click-outside": "^1.1.0", @@ -18547,16 +18546,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/tiptap-text-direction": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tiptap-text-direction/-/tiptap-text-direction-0.3.2.tgz", - "integrity": "sha512-EBiJq4urei+joaBqhPUwaZ7bfmsSf62Lzd4KXm3T0kjtDC8pj3EFoXdi/9MEvbTsiY8iqx3S/kfRInRCMiN7ng==", - "license": "MIT", - "peerDependencies": { - "@tiptap/core": "^2.0.0", - "@tiptap/pm": "^2.0.0" - } - }, "node_modules/tldts": { "version": "6.1.76", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.76.tgz", @@ -33239,12 +33228,6 @@ "@popperjs/core": "^2.9.0" } }, - "tiptap-text-direction": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tiptap-text-direction/-/tiptap-text-direction-0.3.2.tgz", - "integrity": "sha512-EBiJq4urei+joaBqhPUwaZ7bfmsSf62Lzd4KXm3T0kjtDC8pj3EFoXdi/9MEvbTsiY8iqx3S/kfRInRCMiN7ng==", - "requires": {} - }, "tldts": { "version": "6.1.76", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.76.tgz", diff --git a/package.json b/package.json index 40ed07982b9..a98d1f82e2f 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "proxy-polyfill": "^0.3.2", "slug": "^11.0.0", "tippy.js": "^6.3.7", - "tiptap-text-direction": "^0.3.2", "uuid": "^11.1.0", "vue": "^2.7.16", "vue-click-outside": "^1.1.0", diff --git a/src/css/prosemirror.scss b/src/css/prosemirror.scss index 3a481202566..c6b5497d3c2 100644 --- a/src/css/prosemirror.scss +++ b/src/css/prosemirror.scss @@ -51,7 +51,7 @@ div.ProseMirror { display: flex; align-items: start; // Leave space for checkbox (14px width + 2x 1px border + 6px margin-right) - margin-left: -24px; + margin-inline-start: -24px; input[type='checkbox'] { display: none; @@ -262,7 +262,7 @@ div.ProseMirror { pre.frontmatter { margin-bottom: 2em; - border-left: 4px solid var(--color-primary-element); + border-inline-start: 4px solid var(--color-primary-element); } pre.frontmatter::before { @@ -280,7 +280,7 @@ div.ProseMirror { li { position: relative; - padding-left: 3px; + padding-inline-start: 3px; p { position: relative; @@ -298,8 +298,8 @@ div.ProseMirror { ul, ol { - padding-left: 10px; - margin-left: 10px; + padding-inline-start: 10px; + margin-inline-start: 10px; margin-bottom: 1em; } @@ -318,11 +318,10 @@ div.ProseMirror { } blockquote { - padding-left: 1em; - border-left: 4px solid var(--color-primary-element); + padding-inline-start: 1em; + border-inline-start: 4px solid var(--color-primary-element); color: var(--color-text-maxcontrast); - margin-left: 0; - margin-right: 0; + margin-inline: 0; } // table variables diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js index ce676a95c13..c0d19c7be3e 100644 --- a/src/extensions/RichText.js +++ b/src/extensions/RichText.js @@ -16,7 +16,6 @@ import Gapcursor from '@tiptap/extension-gapcursor' import HorizontalRule from '@tiptap/extension-horizontal-rule' import ListItem from '@tiptap/extension-list-item' import Text from '@tiptap/extension-text' -import TextDirection from 'tiptap-text-direction' import MentionSuggestion from '../components/Suggestion/Mention/suggestions.js' import Heading from '../nodes/Heading.js' import EmojiSuggestion from './../components/Suggestion/Emoji/suggestions.js' @@ -25,6 +24,7 @@ import LinkPicker from './../extensions/LinkPicker.js' import Markdown from './../extensions/Markdown.js' import Mention from './../extensions/Mention.js' import Search from './../extensions/Search.js' +import TextDirection from './../extensions/TextDirection.ts' import BulletList from './../nodes/BulletList.js' import Callouts from './../nodes/Callouts.js' import CodeBlock from './../nodes/CodeBlock.js' @@ -120,7 +120,13 @@ export default Extension.create({ LinkBubble, TrailingNode, TextDirection.configure({ - types: ['heading', 'paragraph', 'listItem', 'orderedList'], + types: [ + 'heading', + 'paragraph', + 'listItem', + 'taskItem', + 'blockquote', + ], }), ] const additionalExtensionNames = this.options.extensions.map((e) => e.name) diff --git a/src/extensions/TextDirection.ts b/src/extensions/TextDirection.ts new file mode 100644 index 00000000000..ccb0682e413 --- /dev/null +++ b/src/extensions/TextDirection.ts @@ -0,0 +1,176 @@ +/** + * SPDX-FileCopyrightText: 2023 Amir Hossein Hashemi + * SPDX-License-Identifier: MIT + */ + +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' + +const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC' +const LTR = + 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' + + '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' + + '\uFE00-\uFE6F\uFEFD-\uFFFF' + +/* eslint-disable no-misleading-character-class */ +const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']') +const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']') + +/** + * @param text Text string + * + *Source: https://github.com/facebook/lexical/blob/429e3eb5b5a244026fa4776650aabe3c8e17536b/packages/lexical/src/LexicalUtils.ts#L163 + */ +export function getTextDirection(text: string): 'ltr' | 'rtl' | null { + if (text.length === 0) { + return null + } + if (RTL_REGEX.test(text)) { + return 'rtl' + } + if (LTR_REGEX.test(text)) { + return 'ltr' + } + return null +} + +const validDirections = ['ltr', 'rtl', 'auto'] as const + +type Direction = (typeof validDirections)[number] + +/** + * @param object Property object + * @param object.types List of node types to consider + */ +function TextDirectionPlugin({ types }: { types: string[] }) { + return new Plugin({ + key: new PluginKey('textDirection'), + appendTransaction: (transactions, oldState, newState) => { + const docChanges = transactions.some( + (transaction) => transaction.docChanged, + ) + if (!docChanges) { + return + } + + let modified = false + const tr = newState.tr + tr.setMeta('addToHistory', false) + + newState.doc.descendants((node, pos) => { + if (types.includes(node.type.name)) { + if (node.attrs.dir !== null && node.textContent.length > 0) { + return + } + const newTextDirection = getTextDirection(node.textContent) + if (node.attrs.dir === newTextDirection) { + return + } + + const marks = tr.storedMarks || [] + tr.setNodeAttribute(pos, 'dir', newTextDirection) + // `tr.setNodeAttribute` resets the stored marks so we'll restore them + for (const mark of marks) { + tr.addStoredMark(mark) + } + modified = true + } + }) + + return modified ? tr : null + }, + }) +} + +declare module '@tiptap/core' { + interface Commands { + textDirection: { + /** + * Set the text direction attribute + */ + setTextDirection: (direction: Direction) => ReturnType + /** + * Unset the text direction attribute + */ + unsetTextDirection: () => ReturnType + } + } +} + +export interface TextDirectionOptions { + types: string[] + defaultDirection: Direction | null +} + +export const TextDirection = Extension.create({ + name: 'textDirection', + + addOptions() { + return { + types: [], + defaultDirection: null, + } + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + dir: { + default: null, + parseHTML: (element) => + element.dir || this.options.defaultDirection, + renderHTML: (attributes) => { + if (attributes.dir === this.options.defaultDirection) { + return {} + } + return { dir: attributes.dir } + }, + }, + }, + }, + ] + }, + + addCommands() { + return { + setTextDirection: + (direction: Direction) => + ({ commands }) => { + if (!validDirections.includes(direction)) { + return false + } + + return this.options.types.every((type) => + commands.updateAttributes(type, { dir: direction }), + ) + }, + + unsetTextDirection: + () => + ({ commands }) => { + return this.options.types.every((type) => + commands.resetAttributes(type, 'dir'), + ) + }, + } + }, + + addKeyboardShortcuts() { + return { + 'Mod-Alt-l': () => this.editor.commands.setTextDirection('ltr'), + 'Mod-Alt-r': () => this.editor.commands.setTextDirection('rtl'), + } + }, + + addProseMirrorPlugins() { + return [ + TextDirectionPlugin({ + types: this.options.types, + }), + ] + }, +}) + +export default TextDirection diff --git a/src/nodes/Callout.vue b/src/nodes/Callout.vue index d4e23f59300..6672d679c05 100644 --- a/src/nodes/Callout.vue +++ b/src/nodes/Callout.vue @@ -51,12 +51,12 @@ export default {