feat(text-editor): add highlight, subscript, superscript and mathematics toolbar actions (#10627)

* feat(text-editor): add highlight action

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* feat(text-editor): add subscript and superscript actions

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* feat(text-editor): add mathematics inline and block actions

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* chore(text-editor): add mathematics and katex dependencies

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* fix(text-editor): fix eslint strict-boolean-expressions in mathematics extension

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* fix(text-editor): change max-width of toolbar to display all icons correctly

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* chore(text-editor): add subscript and superscript as direct dependencies

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* chore(text-editor): add missing locale keys for new text editor actions [temp, AI-generated]

Adds placeholder translations (English fallback) for Highlight, Subscript,  Superscript, Mathematics, and MathematicsBlock to all non-English locale files. Translations are not reviewed and should be replaced by a native speaker.

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

* fix(text-editor): fix subscript/superscript serialization and extension placement

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>

---------

Signed-off-by: ComputerCrack <github.compacter838@simplelogin.com>
Co-authored-by: ComputerCrack <github.compacter838@simplelogin.com>
This commit is contained in:
ComputerCrack
2026-03-19 09:51:24 +07:00
committed by GitHub
co-authored by ComputerCrack
parent 83fdb49d23
commit 2d26c74874
28 changed files with 590 additions and 27 deletions
+64 -1
View File
@@ -4122,6 +4122,12 @@ importers:
'@tiptap/extension-strike':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-subscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-superscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-table':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
@@ -28387,12 +28393,21 @@ importers:
'@tiptap/extension-list-keymap':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-mathematics':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(katex@0.16.25)
'@tiptap/extension-ordered-list':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-placeholder':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-subscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-superscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-table':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
@@ -28432,6 +28447,9 @@ importers:
fast-equals:
specifier: ^5.2.2
version: 5.3.2
katex:
specifier: ^0.16.0
version: 0.16.25
lib0:
specifier: ^0.2.109
version: 0.2.114
@@ -28475,6 +28493,9 @@ importers:
'@types/jest':
specifier: ^29.5.5
version: 29.5.14
'@types/katex':
specifier: ^0.16.0
version: 0.16.8
'@typescript-eslint/eslint-plugin':
specifier: ^6.21.0
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
@@ -39062,6 +39083,12 @@ importers:
'@tiptap/extension-placeholder':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-subscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-superscript':
specifier: ^2.11.7
version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-table':
specifier: ^2.11.7
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
@@ -44385,6 +44412,13 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-mathematics@2.27.2':
resolution: {integrity: sha512-ouEMrA7we5p1v7SbHFSrrVQBAUb1moiUOwo0BqdCJXV79YOxdURn+8vlA2rIItO0E3Ya1UFoA1LW+AE0cYn4RQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
katex: ^0.16.4
'@tiptap/extension-mention@2.27.1':
resolution: {integrity: sha512-8qwPIum0rMK/7BaHEN0+M/VkUn00LqhqyRO8JdC/EBVrUBgxKTQsUhIhSstgVAYzD1w7cCUfTFX4bYu9lcFMEQ==}
peerDependencies:
@@ -44413,6 +44447,16 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-subscript@2.27.2':
resolution: {integrity: sha512-x2Oz7hrI4KvzzB9pWChFRm6JnKdYAUQDyrlSROngtzXT7VpNQNoD5s8OlICzDeNsaRKzhR8omIz2z17S1VB48g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-superscript@2.27.2':
resolution: {integrity: sha512-VTGJDuNqdesibSVW94Q71VaGVGr/bwBppdaNLn7k6beOegALfIH7ncArlkD/eihOlJ2qaWiT7FoWNLTb/Fdv1w==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-table-cell@2.27.1':
resolution: {integrity: sha512-VowNmz1kub2qfntWkU8jGA6DoCl9xjJBWSypuQIeiN/IRId3BMrJodT26pTNJ3ChDMtYaanWaUvYqckRxgTC2A==}
peerDependencies:
@@ -44792,6 +44836,9 @@ packages:
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/katex@0.16.8':
resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==}
'@types/keygrip@1.0.6':
resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==}
@@ -55388,6 +55435,12 @@ snapshots:
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-mathematics@2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(katex@0.16.25)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
katex: 0.16.25
'@tiptap/extension-mention@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(@tiptap/suggestion@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
@@ -55411,6 +55464,14 @@ snapshots:
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-subscript@2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-superscript@2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-table-cell@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
@@ -55880,6 +55941,8 @@ snapshots:
'@types/ms': 2.1.0
'@types/node': 22.19.0
'@types/katex@0.16.8': {}
'@types/keygrip@1.0.6': {}
'@types/keyv@3.1.4':
@@ -61383,7 +61446,7 @@ snapshots:
node-loader@2.0.0(webpack@5.102.1):
dependencies:
loader-utils: 2.0.4
webpack: 5.102.1
webpack: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4)
node-localstorage@2.2.1:
dependencies:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/desktop",
"version": "0.7.208",
"version": "0.7.327",
"main": "dist/main/electron.js",
"template": "@hcengineering/webpack-package",
"scripts": {
@@ -421,6 +421,20 @@ export const storeMarks: Record<string, MarkProcessor> = {
mixable: false,
expelEnclosingWhitespace: false,
escape: false
},
subscript: {
open: '<sub>',
close: '</sub>',
mixable: true,
expelEnclosingWhitespace: false,
escape: true
},
superscript: {
open: '<sup>',
close: '</sup>',
mixable: true,
expelEnclosingWhitespace: false,
escape: true
}
}
+3 -1
View File
@@ -80,7 +80,9 @@
"prosemirror-codemark": "^0.4.2",
"fast-equals": "^5.2.2",
"@tiptap/extension-text-align": "~2.11.0",
"@tiptap/extension-text-style": "~2.11.0"
"@tiptap/extension-text-style": "~2.11.0",
"@tiptap/extension-subscript": "^2.11.7",
"@tiptap/extension-superscript": "^2.11.7"
},
"repository": {
"type": "git",
@@ -35,6 +35,9 @@ export { ListItem, type ListItemOptions } from '@tiptap/extension-list-item'
export { OrderedList, type OrderedListOptions } from '@tiptap/extension-ordered-list'
export { TextAlign, type TextAlignOptions } from '@tiptap/extension-text-align'
export { TaskList, type TaskListOptions } from '@tiptap/extension-task-list'
export { Highlight, type HighlightOptions } from '@tiptap/extension-highlight'
export { Subscript } from '@tiptap/extension-subscript'
export { Superscript } from '@tiptap/extension-superscript'
export { Table, type TableOptions } from '@tiptap/extension-table'
export { TableCell, type TableCellOptions } from '@tiptap/extension-table-cell'
+66
View File
@@ -246,6 +246,72 @@ export function createModel (builder: Builder): void {
index: 20
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: {
command: 'toggleHighlight'
},
icon: textEditor.icon.Highlight,
visibilityTester: textEditor.function.IsEditable,
isActive: {
name: 'highlight'
},
label: textEditor.string.Highlight,
category: 20,
index: 22
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: {
command: 'toggleSubscript'
},
icon: textEditor.icon.Subscript,
visibilityTester: textEditor.function.IsEditable,
isActive: {
name: 'subscript'
},
label: textEditor.string.Subscript,
category: 20,
index: 28
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: {
command: 'toggleSuperscript'
},
icon: textEditor.icon.Superscript,
visibilityTester: textEditor.function.IsEditable,
isActive: {
name: 'superscript'
},
label: textEditor.string.Superscript,
category: 20,
index: 30
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: {
command: 'insertMathInline'
},
icon: textEditor.icon.Mathematics,
visibilityTester: textEditor.function.IsEditable,
isActive: textEditor.function.IsMathInlineActive,
label: textEditor.string.Mathematics,
category: 60,
index: 15
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: {
command: 'insertMathBlock'
},
icon: textEditor.icon.MathematicsBlock,
visibilityTester: textEditor.function.IsEditable,
isActive: textEditor.function.IsMathBlockActive,
label: textEditor.string.MathematicsBlock,
category: 60,
index: 20
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
action: textEditor.function.SetTextColor,
icon: textEditor.icon.TextStyle,
+4 -1
View File
@@ -60,6 +60,9 @@ export default mergeIds(textEditorId, textEditor, {
CopyPreviewLinkAction: '' as Resource<TextActionFunction>,
SetBackgroundColor: '' as Resource<TextActionFunction>,
SetTextColor: '' as Resource<TextActionFunction>
SetTextColor: '' as Resource<TextActionFunction>,
IsMathInlineActive: '' as Resource<TextActionActiveFunction>,
IsMathBlockActive: '' as Resource<TextActionActiveFunction>
}
})
@@ -246,6 +246,24 @@
<path d="M8 5a.5.5 0 0 1 .5.5V7H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V8H6a.5.5 0 0 1 0-1h1.5V5.5A.5.5 0 0 1 8 5zm-2.5 6.5A.5.5 0 0 1 6 11h4a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</symbol>
<!-- Material Design Icons, Apache 2.0, https://github.com/Templarian/MaterialDesign -->
<symbol id="subscript" viewBox="0 0 24 24">
<path d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,21.03H16.97V20.03L17.86,19.23C18.62,18.58 19.18,18.04 19.56,17.6C19.93,17.16 20.12,16.75 20.13,16.36C20.14,16.08 20.05,15.85 19.86,15.66C19.68,15.5 19.39,15.38 19,15.38C18.69,15.38 18.42,15.44 18.16,15.56L17.5,15.94L17.05,14.77C17.32,14.56 17.64,14.38 18.03,14.24C18.42,14.1 18.85,14 19.32,14C20.1,14.04 20.7,14.25 21.1,14.66C21.5,15.07 21.72,15.59 21.72,16.23C21.71,16.79 21.53,17.31 21.18,17.78C20.84,18.25 20.42,18.7 19.91,19.14L19.27,19.66V19.68H21.85V21.03Z"/>
</symbol>
<symbol id="superscript" viewBox="0 0 24 24">
<path d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,9H16.97V8L17.86,7.18C18.62,6.54 19.18,6 19.56,5.55C19.93,5.11 20.12,4.7 20.13,4.32C20.14,4.04 20.05,3.8 19.86,3.62C19.68,3.43 19.39,3.34 19,3.33C18.69,3.34 18.42,3.4 18.16,3.5L17.5,3.89L17.05,2.72C17.32,2.5 17.64,2.33 18.03,2.19C18.42,2.05 18.85,2 19.32,2C20.1,2 20.7,2.2 21.1,2.61C21.5,3 21.72,3.54 21.72,4.18C21.71,4.74 21.53,5.26 21.18,5.73C20.84,6.21 20.42,6.66 19.91,7.09L19.27,7.61V7.63H21.85V9Z"/>
</symbol>
<symbol id="highlight" viewBox="0 0 32 32">
<path d="M27.07 4.93a3.16 3.16 0 0 0-4.47 0L9.5 18.03l-.12.14-2.5 6.5a1 1 0 0 0 1.31 1.31l6.5-2.5.14-.12 13.1-13.1a3.16 3.16 0 0 0 0-4.47zM13.1 21.56l-3.66 1.41 1.4-3.66.98-.97 2.26 2.26-.98.96zm2.4-2.39-2.27-2.26 8.6-8.6 2.26 2.27-8.59 8.59zm9.95-9.95-.95.95-2.26-2.26.94-.95a1.16 1.16 0 0 1 1.64 0l.63.63a1.16 1.16 0 0 1 0 1.63z"/>
<rect x="4" y="27" width="24" height="2" rx="1"/>
</symbol>
<!-- Material Design Icons, Apache 2.0, https://github.com/Templarian/MaterialDesign -->
<symbol id="mathematics" viewBox="0 0 24 24">
<path d="M18,6H8.83L14.83,12L8.83,18H18V20H6V18L12,12L6,6V4H18V6Z"/>
</symbol>
<symbol id="mathematicsBlock" viewBox="0 0 24 24">
<path d="M18,6H8.83L14.83,12L8.83,18H18V20H6V18L12,12L6,6V4H18V6ZM2,1H22V3H2ZM2,21H22V23H2Z"/>
</symbol>
<symbol id="seeOriginal" viewBox="0 0 32 32">
<path d="M30.94 15.6599C29.7638 12.6175 27.722 9.98649 25.0668 8.09196C22.4116 6.19743 19.2595 5.12245 16 4.99988C12.7405 5.12245 9.58839 6.19743 6.9332 8.09196C4.27802 9.98649 2.23616 12.6175 1.06 15.6599C0.980569 15.8796 0.980569 16.1202 1.06 16.3399C2.23616 19.3822 4.27802 22.0133 6.9332 23.9078C9.58839 25.8023 12.7405 26.8773 16 26.9999C19.2595 26.8773 22.4116 25.8023 25.0668 23.9078C27.722 22.0133 29.7638 19.3822 30.94 16.3399C31.0194 16.1202 31.0194 15.8796 30.94 15.6599ZM16 24.9999C10.7 24.9999 5.1 21.0699 3.07 15.9999C5.1 10.9299 10.7 6.99988 16 6.99988C21.3 6.99988 26.9 10.9299 28.93 15.9999C26.9 21.0699 21.3 24.9999 16 24.9999Z"/>
<path d="M16 9.99988C14.8133 9.99988 13.6533 10.3518 12.6666 11.0111C11.6799 11.6703 10.9109 12.6074 10.4567 13.7038C10.0026 14.8001 9.88378 16.0065 10.1153 17.1704C10.3468 18.3343 10.9182 19.4034 11.7574 20.2425C12.5965 21.0816 13.6656 21.6531 14.8295 21.8846C15.9933 22.1161 17.1997 21.9973 18.2961 21.5432C19.3925 21.089 20.3295 20.32 20.9888 19.3333C21.6481 18.3466 22 17.1866 22 15.9999C22 14.4086 21.3679 12.8825 20.2426 11.7572C19.1174 10.632 17.5913 9.99988 16 9.99988ZM16 19.9999C15.2089 19.9999 14.4355 19.7653 13.7777 19.3258C13.1199 18.8862 12.6072 18.2615 12.3045 17.5306C12.0017 16.7997 11.9225 15.9954 12.0769 15.2195C12.2312 14.4436 12.6122 13.7309 13.1716 13.1715C13.731 12.612 14.4437 12.2311 15.2196 12.0767C15.9956 11.9224 16.7998 12.0016 17.5307 12.3044C18.2616 12.6071 18.8864 13.1198 19.3259 13.7776C19.7654 14.4354 20 15.2088 20 15.9999C20 17.0607 19.5786 18.0782 18.8284 18.8283C18.0783 19.5785 17.0609 19.9999 16 19.9999Z"/>

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+7 -2
View File
@@ -81,6 +81,11 @@
"UnableToLoadEmbeddedContent": "Náhled odkazu nelze načíst kvůli nastavení oprávnění nebo nepodporovanému obsahu",
"CannotConnectToCollaborationService": "Nelze se připojit k službě spolupráce",
"SourceURL": "Zdrojová URL",
"SelectedDocuments": "Vybrané dokumenty ({count, plural, =1 {1 položka} other {# položek}})"
"SelectedDocuments": "Vybrané dokumenty ({count, plural, =1 {1 položka} other {# položek}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
}
+7 -2
View File
@@ -80,6 +80,11 @@
"UnableToLoadEmbeddedContent": "Die Linkvorschau konnte aufgrund von Berechtigungseinstellungen oder nicht unterstütztem Inhalt nicht geladen werden",
"CannotConnectToCollaborationService": "Kann keine Verbindung zum Kollaborationsdienst herstellen",
"SourceURL": "Quell-URL",
"SelectedDocuments": "Ausgewählte Dokumente ({count, plural, =1 {1 Element} other {# Elemente}})"
"SelectedDocuments": "Ausgewählte Dokumente ({count, plural, =1 {1 Element} other {# Elemente}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "MathematiK",
"MathematicsBlock": "Mathematik Block"
}
}
}
+5
View File
@@ -29,6 +29,11 @@
"GIF": "GIF",
"Mention": "Mention",
"Underlined": "Underlined",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block",
"AlignCenter": "Align center",
"AlignLeft": "Align left",
"AlignRight": "Align right",
+8 -3
View File
@@ -21,7 +21,7 @@
"Update": "Actualizar",
"Remove": "Eliminar",
"NotePlaceholder": "Añadir una nota...",
"SampleText": "Texto de ejemplo",
"SampleText": "Texto de ejemplo",
"Send": "Enviar",
"Attach": "Adjuntar",
"TextStyle": "Estilo de texto",
@@ -71,6 +71,11 @@
"UnableToLoadEmbeddedContent": "No se pudo cargar la vista previa del enlace debido a la configuración de permisos o contenido no compatible",
"CannotConnectToCollaborationService": "No se puede conectar con el colaborador",
"SourceURL": "URL de origen",
"SelectedDocuments": "Documentos seleccionados ({count, plural, =1 {1 elemento} other {# elementos}})"
"SelectedDocuments": "Documentos seleccionados ({count, plural, =1 {1 elemento} other {# elementos}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
}
+7 -2
View File
@@ -71,6 +71,11 @@
"UnableToLoadEmbeddedContent": "Laperçu du lien na pas pu être chargé en raison des paramètres dautorisation ou dun contenu non pris en charge",
"CannotConnectToCollaborationService": "Impossible de se connecter au collaborateur",
"SourceURL": "URL source",
"SelectedDocuments": "Documents sélectionnés ({count, plural, =1 {1 élément} other {# éléments}})"
"SelectedDocuments": "Documents sélectionnés ({count, plural, =1 {1 élément} other {# éléments}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
}
+7 -2
View File
@@ -80,6 +80,11 @@
"UnableToLoadEmbeddedContent": "Impossibile caricare l'anteprima del link a causa delle impostazioni dei permessi o di contenuto non supportato",
"CannotConnectToCollaborationService": "Impossibile connettersi al servizio di collaborazione",
"SourceURL": "URL di origine",
"SelectedDocuments": "Documenti selezionati ({count, plural, =1 {1 elemento} other {# elementi}})"
"SelectedDocuments": "Documenti selezionati ({count, plural, =1 {1 elemento} other {# elementi}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
}
+6 -1
View File
@@ -81,6 +81,11 @@
"UnableToLoadEmbeddedContent": "リンクのプレビューを読み込めません。権限設定または非対応のコンテンツが原因です",
"CannotConnectToCollaborationService": "コラボレーションサービスに接続できません",
"SourceURL": "ソースURL",
"SelectedDocuments": "選択されたドキュメント ({count, plural, =1 {1 項目} other {# 項目}})"
"SelectedDocuments": "選択されたドキュメント ({count, plural, =1 {1 項目} other {# 項目}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
+6 -1
View File
@@ -71,6 +71,11 @@
"UnableToLoadEmbeddedContent": "Não foi possível carregar a pré-visualização do link devido às permissões ou a conteúdo não suportado",
"CannotConnectToCollaborationService": "Não foi possível conectar ao serviço de colaboração",
"SourceURL": "URL de origem",
"SelectedDocuments": "Documentos selecionados ({count, plural, =1 {1 item} other {# itens}})"
"SelectedDocuments": "Documentos selecionados ({count, plural, =1 {1 item} other {# itens}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
+6 -1
View File
@@ -71,6 +71,11 @@
"UnableToLoadEmbeddedContent": "Não foi possível carregar a pré-visualização do link devido às permissões ou a conteúdo não suportado",
"CannotConnectToCollaborationService": "Não foi possível conectar ao serviço de colaboração",
"SourceURL": "URL de origem",
"SelectedDocuments": "Documentos selecionados ({count, plural, =1 {1 item} other {# itens}})"
"SelectedDocuments": "Documentos selecionados ({count, plural, =1 {1 item} other {# itens}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
+7 -2
View File
@@ -81,6 +81,11 @@
"UnableToLoadEmbeddedContent": "Не удалось загрузить превью ссылки из-за настроек доступа или неподдерживаемого содержимого",
"CannotConnectToCollaborationService": "Не удалось подключиться к сервису совместной работы",
"SourceURL": "Исходный URL",
"SelectedDocuments": "Выбранные документы ({count, plural, =1 {1 элемент} other {# элементов}})"
"SelectedDocuments": "Выбранные документы ({count, plural, =1 {1 элемент} other {# элементов}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
}
+6 -1
View File
@@ -81,6 +81,11 @@
"UnableToLoadEmbeddedContent": "Bağlantı önizlemesi izin ayarları veya desteklenmeyen içerik nedeniyle yüklenemedi",
"CannotConnectToCollaborationService": "İşbirliği servisine bağlanılamıyor",
"SourceURL": "Kaynak URL",
"SelectedDocuments": "Seçilen belgeler ({count, plural, =1 {1 öğe} other {# öğe}})"
"SelectedDocuments": "Seçilen belgeler ({count, plural, =1 {1 öğe} other {# öğe}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
+6 -3
View File
@@ -39,7 +39,6 @@
"FullDescription": "完整描述",
"NoFullDescription": "没有详细描述",
"EnableDiffMode": "差异模式",
"AddColumnBefore": "在前面添加",
"AddColumnAfter": "在后面添加",
"DeleteColumn": "删除",
@@ -48,7 +47,6 @@
"DeleteRow": "删除",
"DeleteTable": "删除",
"Duplicate": "重复",
"CategoryRow": "行",
"CategoryColumn": "列",
"Table": "表格",
@@ -73,6 +71,11 @@
"UnableToLoadEmbeddedContent": "由于权限设置或不支持的内容,无法加载链接预览",
"CannotConnectToCollaborationService": "无法连接到协作者",
"SourceURL": "源URL",
"SelectedDocuments": "选定的文档 ({count, plural, =1 {1 项} other {# 项}})"
"SelectedDocuments": "选定的文档 ({count, plural, =1 {1 项} other {# 项}})",
"Highlight": "Highlight",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Mathematics": "Mathematics",
"MathematicsBlock": "Mathematics block"
}
}
+5
View File
@@ -46,6 +46,11 @@ loadMetadata(textEditor.icon, {
MergeCells: `${icons}#union`,
SplitCells: `${icons}#divide`,
Brush: `${icons}#brush`,
Highlight: `${icons}#highlight`,
Subscript: `${icons}#subscript`,
Superscript: `${icons}#superscript`,
Mathematics: `${icons}#mathematics`,
MathematicsBlock: `${icons}#mathematicsBlock`,
TextStyle: `${icons}#textStyle`,
LinkPreview: `${icons}#link`,
Refresh: `${icons}#refresh`,
@@ -36,6 +36,7 @@
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/diff": "~5.0.2",
"@types/katex": "^0.16.0",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
@@ -73,6 +74,8 @@
"@tiptap/extension-code": "^2.11.7",
"@tiptap/extension-code-block": "^2.11.7",
"@tiptap/extension-highlight": "^2.11.7",
"@tiptap/extension-subscript": "^2.11.7",
"@tiptap/extension-superscript": "^2.11.7",
"@tiptap/extension-typography": "^2.11.7",
"@tiptap/extension-link": "^2.11.7",
"@tiptap/extension-ordered-list": "^2.11.7",
@@ -98,6 +101,8 @@
"tippy.js": "~6.3.7",
"@hcengineering/chunter": "workspace:^0.7.0",
"@tiptap/extension-text-align": "~2.11.0",
"@tiptap/extension-mathematics": "^2.11.7",
"katex": "^0.16.0",
"@hcengineering/workbench": "workspace:^0.7.0",
"@hcengineering/drive": "workspace:^0.7.0",
"@hcengineering/time": "workspace:^0.7.0",
@@ -0,0 +1,301 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type Editor } from '@tiptap/core'
import Mathematics from '@tiptap/extension-mathematics'
import katex from 'katex'
import { type Node } from '@tiptap/pm/model'
import { type EditorState, Plugin, PluginKey, type Transaction, TextSelection } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
mathematics: {
insertMathInline: () => ReturnType
insertMathBlock: () => ReturnType
}
}
}
function renderKatex (content: string, element: HTMLElement, options: katex.KatexOptions): void {
try {
katex.render(content, element, options)
} catch {
element.innerHTML = content
}
}
function findInlineMathBoundaries (state: EditorState): { before: number, after: number } | null {
const { from } = state.selection
const $pos = state.doc.resolve(from)
const parentStart = $pos.start()
const parent = $pos.parent
const text = parent.textBetween(0, parent.content.size, null, '\n')
const posInParent = from - parentStart
const before = text.lastIndexOf('$', posInParent - 1)
if (before === -1) return null
if (before > 0 && text[before - 1] === '$') return null
const after = text.indexOf('$', posInParent)
if (after === -1 || after < posInParent) return null
if (after + 1 < text.length && text[after + 1] === '$') return null
return { before: parentStart + before, after: parentStart + after }
}
function wrapWithMath (
state: EditorState,
dispatch: ((tr: Transaction) => void) | undefined,
delimiter: string
): boolean {
const { selection } = state
const { from, to } = selection
const dLen = delimiter.length
const text = state.doc.textBetween(from, to)
const charsBefore = from >= dLen ? state.doc.textBetween(from - dLen, from) : ''
const charsAfter = to + dLen <= state.doc.content.size ? state.doc.textBetween(to, to + dLen) : ''
if (charsBefore === delimiter && charsAfter === delimiter) {
if (dispatch !== undefined) dispatch(state.tr.delete(to, to + dLen).delete(from - dLen, from))
return true
}
const longerDelimiter = delimiter.repeat(2)
if (
text.startsWith(delimiter) &&
text.endsWith(delimiter) &&
!text.startsWith(longerDelimiter) &&
!text.endsWith(longerDelimiter)
) {
if (dispatch !== undefined) dispatch(state.tr.delete(to - dLen, to).delete(from, from + dLen))
return true
}
if (dispatch !== undefined) dispatch(state.tr.insertText(delimiter, to).insertText(delimiter, from))
return true
}
export async function isMathInlineActive (editor: Editor): Promise<boolean> {
const { state } = editor
const { selection } = state
const { from, to } = selection
if (!selection.empty) {
const text = state.doc.textBetween(from, to)
if (text.startsWith('$') && !text.startsWith('$$') && text.endsWith('$') && !text.endsWith('$$')) return true
const before = from > 0 ? state.doc.textBetween(from - 1, from) : ''
const after = to < state.doc.content.size ? state.doc.textBetween(to, to + 1) : ''
if (before === '$' && after === '$') {
const charBefore2 = from > 1 ? state.doc.textBetween(from - 2, from - 1) : ''
const charAfter2 = to + 1 < state.doc.content.size ? state.doc.textBetween(to + 1, to + 2) : ''
return charBefore2 !== '$' && charAfter2 !== '$'
}
return false
}
return findInlineMathBoundaries(state) !== null
}
export async function isMathBlockActive (editor: Editor): Promise<boolean> {
const { state } = editor
const { selection } = state
const { from, to } = selection
if (!selection.empty) {
const text = state.doc.textBetween(from, to)
if (text.startsWith('$$') && text.endsWith('$$')) return true
const before = from >= 2 ? state.doc.textBetween(from - 2, from) : ''
const after = to + 2 <= state.doc.content.size ? state.doc.textBetween(to, to + 2) : ''
return before === '$$' && after === '$$'
}
const $pos = state.doc.resolve(from)
const parentStart = $pos.start()
const parent = $pos.parent
const text = parent.textBetween(0, parent.content.size, null, '\n')
const posInParent = from - parentStart
const before = text.lastIndexOf('$$', posInParent - 1)
if (before === -1) return false
const after = text.indexOf('$$', posInParent)
return after !== -1 && after >= posInParent
}
function createMathPlugin (
regex: RegExp,
katexOptions: katex.KatexOptions,
editor: Editor,
shouldRender: (state: EditorState, pos: number, node: Node) => boolean
): Plugin {
return new Plugin({
key: new PluginKey('mathematics-' + regex.source),
state: {
init () {
return { decorations: undefined as DecorationSet | undefined }
},
apply (tr, prev, _oldState, newState) {
if (!tr.docChanged && !tr.selectionSet && prev.decorations !== undefined) {
return prev
}
const { selection } = newState
const isEditable = editor.isEditable
const decorations: Decoration[] = []
newState.doc.nodesBetween(0, newState.doc.content.size, (node, pos) => {
if (!node.inlineContent) return true
if (!shouldRender(newState, pos, node)) return false
const text = node.textBetween(0, node.content.size, null, '\n')
regex.lastIndex = 0
let match: RegExpExecArray | null
while ((match = regex.exec(text)) !== null) {
const from = pos + 1 + match.index
const to = from + match[0].length
const content = match.slice(1).find(Boolean)
if (content === undefined) continue
const isCollapsed = selection.from === selection.to
const anchorIsInside = selection.anchor >= from && selection.anchor <= to
const rangeIsInside = selection.from >= from && selection.to <= to
const isEditing = (isCollapsed && anchorIsInside) || rangeIsInside
decorations.push(
Decoration.inline(
from,
to,
{
class:
isEditing && isEditable
? 'Tiptap-mathematics-editor'
: 'Tiptap-mathematics-editor Tiptap-mathematics-editor--hidden',
style:
!isEditing || !isEditable
? 'display: inline-block; height: 0; opacity: 0; overflow: hidden; position: absolute; width: 0;'
: undefined
},
{ content, isEditable, isEditing }
)
)
if (!isEditable || !isEditing) {
decorations.push(
Decoration.widget(
from,
() => {
const element = document.createElement('span')
element.classList.add('Tiptap-mathematics-render')
if (isEditable) element.classList.add('Tiptap-mathematics-render--editable')
renderKatex(content, element, katexOptions)
return element
},
{ content, isEditable, isEditing }
)
)
}
}
return false
})
return { decorations: DecorationSet.create(newState.doc, decorations) }
}
},
props: {
decorations (state) {
return this.getState(state)?.decorations ?? DecorationSet.empty
}
}
})
}
function MathPastePlugin (): Plugin {
const LATEX_RE = /\$\$?[^$]+\$\$?/
return new Plugin({
props: {
handleDOMEvents: {
paste (view, event) {
const clipboardData = event.clipboardData
if (clipboardData === null) return false
const text = clipboardData.getData('text/plain')
if (text === '' || !LATEX_RE.test(text)) return false
event.preventDefault()
view.dispatch(view.state.tr.insertText(text))
return true
}
}
}
})
}
const shouldRender = (_state: EditorState, _pos: number, node: Node): boolean => {
return node.type.name !== 'codeBlock'
}
export const MathematicsExtension = Mathematics.extend({
addCommands () {
return {
insertMathInline:
() =>
({ state, dispatch }) => {
const { selection } = state
const { from } = selection
if (selection.empty) {
const boundaries = findInlineMathBoundaries(state)
if (boundaries !== null) {
if (dispatch !== undefined) {
dispatch(
state.tr
.delete(boundaries.after, boundaries.after + 1)
.delete(boundaries.before, boundaries.before + 1)
)
}
return true
}
const tr = state.tr.insertText('$ $', from)
if (dispatch !== undefined) dispatch(tr.setSelection(TextSelection.create(tr.doc, from + 2)))
return true
}
return wrapWithMath(state, dispatch, '$')
},
insertMathBlock:
() =>
({ state, dispatch }) => {
const { selection } = state
const { from } = selection
if (selection.empty) {
const tr = state.tr.insertText('$$ $$', from)
if (dispatch !== undefined) dispatch(tr.setSelection(TextSelection.create(tr.doc, from + 3)))
return true
}
return wrapWithMath(state, dispatch, '$$')
}
}
},
addProseMirrorPlugins () {
return [
createMathPlugin(/\$\$([\s\S]+?)\$\$/g, { displayMode: true, throwOnError: false }, this.editor, shouldRender),
createMathPlugin(
/(?<!\$)\$((?:[^$]|\n)+?)\$(?!\$)/g,
{ displayMode: false, throwOnError: false },
this.editor,
shouldRender
),
MathPastePlugin()
]
}
})
@@ -261,7 +261,7 @@ export function ToolbarControlPlugin (editor: Editor, options: ToolbarOptions):
getReferenceClientRect,
inertia: true,
content: container,
maxWidth: 800,
maxWidth: 1000,
interactive: true,
trigger: 'manual',
placement: viewOptions?.placement,
+5 -1
View File
@@ -39,6 +39,7 @@ import {
shouldShowCopyPreviewLinkAction,
copyPreviewLinkAction
} from './components/extension/embed/embed'
import { isMathBlockActive, isMathInlineActive } from './components/extension/mathematics'
import { formatLink, isEditable, isHeadingVisible } from './utils'
export { SmartPasteExtension as TransformPastedContentExtension } from './components/extension/shortcuts/smartPaste'
export { getReferenceFromUrl, getReferenceLabel, getTargetObjectFromUrl } from './components/extension/reference'
@@ -141,6 +142,9 @@ export default async (): Promise<Resources> => ({
CopyPreviewLinkAction: copyPreviewLinkAction,
SetBackgroundColor: openBackgroundColorOptions,
SetTextColor: openTextColorOptions
SetTextColor: openTextColorOptions,
IsMathInlineActive: isMathInlineActive,
IsMathBlockActive: isMathBlockActive
}
})
@@ -23,12 +23,16 @@ import {
CommonListKitFactory,
extensionKit,
mergeKitOptions,
TextColorStylingKit
TextColorStylingKit,
Highlight,
Subscript,
Superscript
} from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { type AnyExtension, Extension } from '@tiptap/core'
import TableHeader from '@tiptap/extension-table-header'
import 'prosemirror-codemark/dist/codemark.css'
import 'katex/dist/katex.min.css'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
@@ -67,6 +71,7 @@ import { ToolbarExtension } from '../components/extension/toolbar/toolbar'
import { ListKeymapExtension } from '../components/extension/shortcuts/listKeymap'
import { I18nPlaceholderExtension } from '../components/extension/i18nPlaceholder'
import { EditorContextExtension } from '../components/extension/editorContext'
import { MathematicsExtension } from '../components/extension/mathematics'
export interface EditorKitContext {
mode?: 'full' | 'compact'
@@ -101,6 +106,10 @@ const StaticEditorKit = extensionKit(
file: e(FileExtension, { inline: true }),
image: e(ImageExtension),
emoji: e(EmojiExtension),
mathematics: e(MathematicsExtension, context.mode === 'full'),
highlight: e(Highlight, { multicolor: false }),
subscript: e(Subscript),
superscript: e(Superscript),
drawingBoard: e(DrawingBoardExtension),
embed: e(
EmbedNode,
+10
View File
@@ -43,6 +43,11 @@ export default plugin(textEditorId, {
GIF: '' as IntlString,
Mention: '' as IntlString,
Underlined: '' as IntlString,
Highlight: '' as IntlString,
Subscript: '' as IntlString,
Superscript: '' as IntlString,
Mathematics: '' as IntlString,
MathematicsBlock: '' as IntlString,
EditorPlaceholder: '' as IntlString,
Edit: '' as IntlString,
Bold: '' as IntlString,
@@ -150,6 +155,11 @@ export default plugin(textEditorId, {
MergeCells: '' as Asset,
SplitCells: '' as Asset,
Brush: '' as Asset,
Highlight: '' as Asset,
Subscript: '' as Asset,
Superscript: '' as Asset,
Mathematics: '' as Asset,
MathematicsBlock: '' as Asset,
TextStyle: '' as Asset,
LinkPreview: '' as Asset,
Refresh: '' as Asset,
+2
View File
@@ -92,6 +92,8 @@
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@tiptap/extension-highlight": "^2.11.7",
"@tiptap/extension-subscript": "^2.11.7",
"@tiptap/extension-superscript": "^2.11.7",
"@tiptap/extension-placeholder": "^2.11.7",
"@tiptap/extension-mention": "^2.11.7",
"@tiptap/extension-typography": "^2.11.7",