diff --git a/package-lock.json b/package-lock.json index a561ea27947..31b9a80ff7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,6 @@ "proxy-polyfill": "^0.3.2", "slug": "^9.1.0", "tippy.js": "^6.3.7", - "tiptap-text-direction": "^0.3.2", "uuid": "^10.0.0", "vue": "^2.7.16", "vue-click-outside": "^1.1.0", @@ -23618,16 +23617,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.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -42691,12 +42680,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.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", diff --git a/package.json b/package.json index 19ed89af8c8..dd52c15a7d4 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,6 @@ "proxy-polyfill": "^0.3.2", "slug": "^9.1.0", "tippy.js": "^6.3.7", - "tiptap-text-direction": "^0.3.2", "uuid": "^10.0.0", "vue": "^2.7.16", "vue-click-outside": "^1.1.0", diff --git a/src/css/prosemirror.scss b/src/css/prosemirror.scss index 09547923e44..afac3ccb99f 100644 --- a/src/css/prosemirror.scss +++ b/src/css/prosemirror.scss @@ -50,7 +50,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; @@ -256,7 +256,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 { @@ -274,7 +274,7 @@ div.ProseMirror { li { position: relative; - padding-left: 3px; + padding-inline-start: 3px; p { position: relative; @@ -291,8 +291,8 @@ div.ProseMirror { } ul, ol { - padding-left: 10px; - margin-left: 10px; + padding-inline-start: 10px; + margin-inline-start: 10px; margin-bottom: 1em; } @@ -311,11 +311,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 9e77015b7d9..e131ad9b1ab 100644 --- a/src/extensions/RichText.js +++ b/src/extensions/RichText.js @@ -41,7 +41,7 @@ import TaskItem from './../nodes/TaskItem.js' import TaskList from './../nodes/TaskList.js' import Text from '@tiptap/extension-text' import TrailingNode from './../nodes/TrailingNode.js' -import TextDirection from 'tiptap-text-direction' +import TextDirection from './../extensions/TextDirection.ts' /* eslint-enable import/no-named-as-default */ import { Strong, Italic, Strike, Link, Underline } from './../marks/index.js' @@ -117,7 +117,13 @@ export default Extension.create({ : null, 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..5142b171306 --- /dev/null +++ b/src/extensions/TextDirection.ts @@ -0,0 +1,175 @@ +/** + * 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 b55579c6de6..6bc58d2af25 100644 --- a/src/nodes/Callout.vue +++ b/src/nodes/Callout.vue @@ -50,12 +50,12 @@ export default {