mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 06:54:23 +01:00
Compare commits
54 Commits
9815033973
...
9a2cb548a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a2cb548a7 | ||
|
|
8eca14069a | ||
|
|
1af0477ac0 | ||
|
|
43920f12ae | ||
|
|
5a0beec6cb | ||
|
|
98241fb54b | ||
|
|
3051664228 | ||
|
|
1ed774365c | ||
|
|
f2e33dfd58 | ||
|
|
90b5282b39 | ||
|
|
d520fc46b9 | ||
|
|
e69b5988ec | ||
|
|
3cdc1ba794 | ||
|
|
25e1008c5c | ||
|
|
a093862311 | ||
|
|
53057ea9fc | ||
|
|
94db96de3e | ||
|
|
60e4fbbf75 | ||
|
|
d35dd67632 | ||
|
|
8813985c68 | ||
|
|
538c98b587 | ||
|
|
389c7029cf | ||
|
|
d47f9e1131 | ||
|
|
c0a8d29756 | ||
|
|
668fd34af6 | ||
|
|
8aa08cf8fe | ||
|
|
16c04f5ae4 | ||
|
|
f8d84814e0 | ||
|
|
c46cf41842 | ||
|
|
64ab1c4116 | ||
|
|
a6de1041c7 | ||
|
|
c8d34e65ea | ||
|
|
51db729546 | ||
|
|
d2052ad236 | ||
|
|
9c4301467f | ||
|
|
e7355dc0e4 | ||
|
|
4110fec94f | ||
|
|
d5e601eae9 | ||
|
|
4f044c4a57 | ||
|
|
5821c350e1 | ||
|
|
edba8188fe | ||
|
|
1471a72633 | ||
|
|
56834cb88a | ||
|
|
a0f16f9184 | ||
|
|
de80eb4806 | ||
|
|
48a4b81fbe | ||
|
|
e225794f72 | ||
|
|
4eef30f8b5 | ||
|
|
569b09609d | ||
|
|
39838c25c2 | ||
|
|
49e90c08a9 | ||
|
|
e777b06fb8 | ||
|
|
497ec2ac74 | ||
|
|
c5d282d203 |
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@ -79,7 +79,7 @@ jobs:
|
|||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: e2e report
|
name: e2e report ${{ matrix.arch }}
|
||||||
path: apps/server-e2e/test-output
|
path: apps/server-e2e/test-output
|
||||||
|
|
||||||
- name: Kill the server
|
- name: Kill the server
|
||||||
|
|||||||
21
apps/client/src/menus/context_menu_utils.ts
Normal file
21
apps/client/src/menus/context_menu_utils.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { t } from "../services/i18n"
|
||||||
|
import attributes from "../services/attributes"
|
||||||
|
import FNote from "../entities/fnote"
|
||||||
|
|
||||||
|
export function getArchiveMenuItem(note: FNote) {
|
||||||
|
if (!note.isArchived) {
|
||||||
|
return {
|
||||||
|
title: t("board_view.archive-note"),
|
||||||
|
uiIcon: "bx bx-archive",
|
||||||
|
handler: () => attributes.addLabel(note.noteId, "archived")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: t("board_view.unarchive-note"),
|
||||||
|
uiIcon: "bx bx-archive-out",
|
||||||
|
handler: async () => {
|
||||||
|
attributes.removeOwnedLabelByName(note, "archived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -207,7 +207,7 @@ function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomString(len: number) {
|
export function randomString(len: number) {
|
||||||
let text = "";
|
let text = "";
|
||||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
|||||||
@ -257,6 +257,11 @@ button.close:hover {
|
|||||||
color: var(--hover-item-text-color);
|
color: var(--hover-item-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.custom-title-bar-button {
|
||||||
|
background: transparent;
|
||||||
|
border: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--modal-background-color) !important;
|
background-color: var(--modal-background-color) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -275,7 +275,8 @@
|
|||||||
--calendar-coll-event-background-lightness: 20%;
|
--calendar-coll-event-background-lightness: 20%;
|
||||||
--calendar-coll-event-background-color: #3c3c3c;
|
--calendar-coll-event-background-color: #3c3c3c;
|
||||||
--calendar-coll-event-text-color: white;
|
--calendar-coll-event-text-color: white;
|
||||||
--calendar-cell-event-hover-filter: brightness(1.25);
|
--calendar-coll-event-hover-filter: brightness(1.25);
|
||||||
|
--callendar-coll-event-archived-sripe-color: #00000026;
|
||||||
--calendar-coll-today-background-color: #ffffff08;
|
--calendar-coll-today-background-color: #ffffff08;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -273,7 +273,8 @@
|
|||||||
--calendar-coll-event-background-saturation: 80%;
|
--calendar-coll-event-background-saturation: 80%;
|
||||||
--calendar-coll-event-background-color: #eaeaea;
|
--calendar-coll-event-background-color: #eaeaea;
|
||||||
--calendar-coll-event-text-color: black;
|
--calendar-coll-event-text-color: black;
|
||||||
--calendar-cell-event-hover-filter: brightness(.95) saturate(1.25);
|
--calendar-coll-event-hover-filter: brightness(.95) saturate(1.25);
|
||||||
|
--callendar-coll-event-archived-sripe-color: #0000000a;
|
||||||
--calendar-coll-today-background-color: #00000006;
|
--calendar-coll-today-background-color: #00000006;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -643,7 +643,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-printable:not(.word-wrap) pre code {
|
.ck-content:not(.word-wrap) pre code {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,7 +162,8 @@
|
|||||||
"inPageSearch": "页面内搜索",
|
"inPageSearch": "页面内搜索",
|
||||||
"newTabWithActivationNoteLink": "在新标签页打开笔记链接并激活该标签页",
|
"newTabWithActivationNoteLink": "在新标签页打开笔记链接并激活该标签页",
|
||||||
"title": "资料表",
|
"title": "资料表",
|
||||||
"newTabNoteLink": "在新标签页开启链接"
|
"newTabNoteLink": "在新标签页开启链接",
|
||||||
|
"editShortcuts": "编辑键盘快捷键"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"importIntoNote": "导入到笔记",
|
"importIntoNote": "导入到笔记",
|
||||||
@ -2104,5 +2105,8 @@
|
|||||||
"clear-color": "清除笔记颜色",
|
"clear-color": "清除笔记颜色",
|
||||||
"set-color": "设置笔记颜色",
|
"set-color": "设置笔记颜色",
|
||||||
"set-custom-color": "设置自定义笔记颜色"
|
"set-custom-color": "设置自定义笔记颜色"
|
||||||
|
},
|
||||||
|
"popup-editor": {
|
||||||
|
"maximize": "切换至完整编辑器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -736,8 +736,8 @@
|
|||||||
"zoom_out_title": "Zoom Out"
|
"zoom_out_title": "Zoom Out"
|
||||||
},
|
},
|
||||||
"zpetne_odkazy": {
|
"zpetne_odkazy": {
|
||||||
"backlink": "{{count}} Backlink",
|
"backlink_one": "{{count}} Backlink",
|
||||||
"backlinks": "{{count}} Backlinks",
|
"backlink_other": "{{count}} Backlinks",
|
||||||
"relation": "relation"
|
"relation": "relation"
|
||||||
},
|
},
|
||||||
"mobile_detail_menu": {
|
"mobile_detail_menu": {
|
||||||
|
|||||||
@ -2105,5 +2105,8 @@
|
|||||||
"clear-color": "ノートの色をクリア",
|
"clear-color": "ノートの色をクリア",
|
||||||
"set-color": "ノートの色を設定",
|
"set-color": "ノートの色を設定",
|
||||||
"set-custom-color": "ノートの色をカスタム設定"
|
"set-custom-color": "ノートの色をカスタム設定"
|
||||||
|
},
|
||||||
|
"popup-editor": {
|
||||||
|
"maximize": "フルエディターに切り替え"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1362,8 +1362,9 @@
|
|||||||
"title": "Factorul de zoom (doar pentru versiunea desktop)"
|
"title": "Factorul de zoom (doar pentru versiunea desktop)"
|
||||||
},
|
},
|
||||||
"zpetne_odkazy": {
|
"zpetne_odkazy": {
|
||||||
"backlink": "{{count}} legături de retur",
|
"backlink_one": "{{count}} legătură de retur",
|
||||||
"backlinks": "{{count}} legături de retur",
|
"backlink_few": "{{count}} legături de retur",
|
||||||
|
"backlink_other": "{{count}} de legături de retur",
|
||||||
"relation": "relație"
|
"relation": "relație"
|
||||||
},
|
},
|
||||||
"svg_export_button": {
|
"svg_export_button": {
|
||||||
|
|||||||
@ -162,7 +162,8 @@
|
|||||||
"inPageSearch": "頁面內搜尋",
|
"inPageSearch": "頁面內搜尋",
|
||||||
"title": "列表",
|
"title": "列表",
|
||||||
"newTabNoteLink": "在新分頁開啟筆記連結",
|
"newTabNoteLink": "在新分頁開啟筆記連結",
|
||||||
"newTabWithActivationNoteLink": "在新分頁開啟並切換至筆記連結"
|
"newTabWithActivationNoteLink": "在新分頁開啟並切換至筆記連結",
|
||||||
|
"editShortcuts": "編輯鍵盤快捷鍵"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"importIntoNote": "匯入至筆記",
|
"importIntoNote": "匯入至筆記",
|
||||||
@ -2104,5 +2105,8 @@
|
|||||||
"clear-color": "清除筆記顏色",
|
"clear-color": "清除筆記顏色",
|
||||||
"set-color": "設定筆記顏色",
|
"set-color": "設定筆記顏色",
|
||||||
"set-custom-color": "設定自訂筆記顏色"
|
"set-custom-color": "設定自訂筆記顏色"
|
||||||
|
},
|
||||||
|
"popup-editor": {
|
||||||
|
"maximize": "切換至完整編輯器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import froca from "../services/froca";
|
|||||||
import NoteLink from "./react/NoteLink";
|
import NoteLink from "./react/NoteLink";
|
||||||
import RawHtml from "./react/RawHtml";
|
import RawHtml from "./react/RawHtml";
|
||||||
import { ViewTypeOptions } from "./collections/interface";
|
import { ViewTypeOptions } from "./collections/interface";
|
||||||
|
import attributes from "../services/attributes";
|
||||||
|
|
||||||
export interface FloatingButtonContext {
|
export interface FloatingButtonContext {
|
||||||
parentComponent: Component;
|
parentComponent: Component;
|
||||||
@ -310,13 +311,24 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
|||||||
let [ popupOpen, setPopupOpen ] = useState(false);
|
let [ popupOpen, setPopupOpen ] = useState(false);
|
||||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function refresh() {
|
||||||
if (!isDefaultViewMode) return;
|
if (!isDefaultViewMode) return;
|
||||||
|
|
||||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||||
setBacklinkCount(resp.count);
|
setBacklinkCount(resp.count);
|
||||||
});
|
});
|
||||||
}, [ note ]);
|
}
|
||||||
|
|
||||||
|
useEffect(() => refresh(), [ note ]);
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
loadResults.getAttributeRows().some(attr =>
|
||||||
|
attr.type === "relation" &&
|
||||||
|
attr.name === "internalLink" &&
|
||||||
|
attributes.isAffecting(attr, note))
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Determine the max height of the container.
|
// Determine the max height of the container.
|
||||||
const { windowHeight } = useWindowSize();
|
const { windowHeight } = useWindowSize();
|
||||||
|
|||||||
@ -14,8 +14,10 @@ import ws from "../services/ws";
|
|||||||
import { UpdateAttributeResponse } from "@triliumnext/commons";
|
import { UpdateAttributeResponse } from "@triliumnext/commons";
|
||||||
import attributes from "../services/attributes";
|
import attributes from "../services/attributes";
|
||||||
import debounce from "../services/debounce";
|
import debounce from "../services/debounce";
|
||||||
|
import { randomString } from "../services/utils";
|
||||||
|
|
||||||
interface Cell {
|
interface Cell {
|
||||||
|
uniqueId: string;
|
||||||
definitionAttr: FAttribute;
|
definitionAttr: FAttribute;
|
||||||
definition: DefinitionObject;
|
definition: DefinitionObject;
|
||||||
valueAttr: Attribute;
|
valueAttr: Attribute;
|
||||||
@ -44,6 +46,7 @@ export default function PromotedAttributes() {
|
|||||||
<div className="promoted-attributes-widget">
|
<div className="promoted-attributes-widget">
|
||||||
{cells && cells.length > 0 && <div className="promoted-attributes-container">
|
{cells && cells.length > 0 && <div className="promoted-attributes-container">
|
||||||
{note && cells?.map(cell => <PromotedAttributeCell
|
{note && cells?.map(cell => <PromotedAttributeCell
|
||||||
|
key={cell.uniqueId}
|
||||||
cell={cell}
|
cell={cell}
|
||||||
cells={cells} setCells={setCells}
|
cells={cells} setCells={setCells}
|
||||||
shouldFocus={cell === cellToFocus} setCellToFocus={setCellToFocus}
|
shouldFocus={cell === cellToFocus} setCellToFocus={setCellToFocus}
|
||||||
@ -103,7 +106,8 @@ function usePromotedAttributeData(note: FNote | null | undefined, componentId: s
|
|||||||
valueAttr.attributeId = "";
|
valueAttr.attributeId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
cells.push({ definitionAttr, definition, valueAttr, valueName });
|
const uniqueId = randomString(10);
|
||||||
|
cells.push({ definitionAttr, definition, valueAttr, valueName, uniqueId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCells(cells);
|
setCells(cells);
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import FNote from "../../../entities/fnote";
|
|||||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import link_context_menu from "../../../menus/link_context_menu";
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
import attributes from "../../../services/attributes";
|
|
||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
import dialog from "../../../services/dialog";
|
import dialog from "../../../services/dialog";
|
||||||
|
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import Api from "./api";
|
import Api from "./api";
|
||||||
|
|
||||||
@ -43,17 +43,6 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
|||||||
items: [
|
items: [
|
||||||
...link_context_menu.getItems(),
|
...link_context_menu.getItems(),
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{
|
|
||||||
title: t("board_view.move-to"),
|
|
||||||
uiIcon: "bx bx-transfer",
|
|
||||||
items: api.columns.map(columnToMoveTo => ({
|
|
||||||
title: columnToMoveTo,
|
|
||||||
enabled: columnToMoveTo !== column,
|
|
||||||
handler: () => api.changeColumn(note.noteId, columnToMoveTo)
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
getArchiveMenuItem(note),
|
|
||||||
{ kind: "separator" },
|
|
||||||
{
|
{
|
||||||
title: t("board_view.insert-above"),
|
title: t("board_view.insert-above"),
|
||||||
uiIcon: "bx bx-list-plus",
|
uiIcon: "bx bx-list-plus",
|
||||||
@ -65,6 +54,17 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
|||||||
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
||||||
},
|
},
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
title: t("board_view.move-to"),
|
||||||
|
uiIcon: "bx bx-transfer",
|
||||||
|
items: api.columns.map(columnToMoveTo => ({
|
||||||
|
title: columnToMoveTo,
|
||||||
|
enabled: columnToMoveTo !== column,
|
||||||
|
handler: () => api.changeColumn(note.noteId, columnToMoveTo)
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{ kind: "separator" },
|
||||||
|
getArchiveMenuItem(note),
|
||||||
{
|
{
|
||||||
title: t("board_view.remove-from-board"),
|
title: t("board_view.remove-from-board"),
|
||||||
uiIcon: "bx bx-task-x",
|
uiIcon: "bx bx-task-x",
|
||||||
@ -85,20 +85,3 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArchiveMenuItem(note: FNote) {
|
|
||||||
if (!note.isArchived) {
|
|
||||||
return {
|
|
||||||
title: t("board_view.archive-note"),
|
|
||||||
uiIcon: "bx bx-archive",
|
|
||||||
handler: () => attributes.addLabel(note.noteId, "archived")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: t("board_view.unarchive-note"),
|
|
||||||
uiIcon: "bx bx-archive-out",
|
|
||||||
handler: async () => {
|
|
||||||
attributes.removeOwnedLabelByName(note, "archived")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,11 +3,10 @@ import FNote from "../../../entities/fnote";
|
|||||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import link_context_menu from "../../../menus/link_context_menu";
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
import froca from "../../../services/froca";
|
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||||
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
|
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@ -17,15 +16,13 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
|||||||
items: [
|
items: [
|
||||||
...link_context_menu.getItems(),
|
...link_context_menu.getItems(),
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
getArchiveMenuItem(note),
|
||||||
{
|
{
|
||||||
title: t("calendar_view.delete_note"),
|
title: t("calendar_view.delete_note"),
|
||||||
uiIcon: "bx bx-trash",
|
uiIcon: "bx bx-trash",
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
const noteToDelete = await froca.getNote(noteId);
|
|
||||||
if (!noteToDelete) return;
|
|
||||||
|
|
||||||
let branchIdToDelete: string | null = null;
|
let branchIdToDelete: string | null = null;
|
||||||
for (const parentBranch of noteToDelete.getParentBranches()) {
|
for (const parentBranch of note.getParentBranches()) {
|
||||||
const parentNote = await parentBranch.getNote();
|
const parentNote = await parentBranch.getNote();
|
||||||
if (parentNote?.hasAncestor(parentNote.noteId)) {
|
if (parentNote?.hasAncestor(parentNote.noteId)) {
|
||||||
branchIdToDelete = parentBranch.branchId;
|
branchIdToDelete = parentBranch.branchId;
|
||||||
@ -40,9 +37,9 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
|||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{
|
{
|
||||||
kind: "custom",
|
kind: "custom",
|
||||||
componentFn: () => NoteColorPicker({note: noteId})
|
componentFn: () => NoteColorPicker({note: note})
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,9 @@
|
|||||||
--calendar-coll-event-background-lightness: 95%;
|
--calendar-coll-event-background-lightness: 95%;
|
||||||
--calendar-coll-event-background-saturation: 80%;
|
--calendar-coll-event-background-saturation: 80%;
|
||||||
--calendar-coll-event-background-color: var(--accented-background-color);
|
--calendar-coll-event-background-color: var(--accented-background-color);
|
||||||
--calendar-coll-event-text-color: var(--primary-button-text-color);
|
--calendar-coll-event-text-color: var(--main-text-color);
|
||||||
--calendar-cell-event-hover-filter: none;
|
--calendar-coll-event-hover-filter: none;
|
||||||
|
--callendar-coll-event-archived-sripe-color: #00000013;
|
||||||
--calendar-coll-today-background-color: var(--more-accented-background-color);
|
--calendar-coll-today-background-color: var(--more-accented-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +54,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-container a.fc-event.archived {
|
.calendar-container a.fc-event.archived {
|
||||||
opacity: 0.5;
|
opacity: .65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-container a.fc-event.archived::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
--c1: transparent;
|
||||||
|
--c2: var(--callendar-coll-event-archived-sripe-color);
|
||||||
|
|
||||||
|
background: repeating-linear-gradient(45deg, var(--c1), var(--c1) 8px,
|
||||||
|
var(--c2) 8px, var(--c2) 16px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-container .fc-button {
|
.calendar-container .fc-button {
|
||||||
@ -129,17 +146,18 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
|||||||
|
|
||||||
.calendar-view a.fc-timegrid-event:hover,
|
.calendar-view a.fc-timegrid-event:hover,
|
||||||
.calendar-view a.fc-daygrid-event:hover {
|
.calendar-view a.fc-daygrid-event:hover {
|
||||||
filter: var(--calendar-cell-event-hover-filter);
|
filter: var(--calendar-coll-event-hover-filter);
|
||||||
border-color: var(--fc-event-text-color);
|
border-color: var(--fc-event-text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-view .fc-timegrid-event.with-hue,
|
.calendar-view .fc-timegrid-event.with-hue,
|
||||||
.calendar-view .fc-daygrid-event.with-hue {
|
.calendar-view .fc-daygrid-event.with-hue {
|
||||||
--fc-event-text-color: var(--custom-color);
|
--fc-event-text-color: var(--custom-color);
|
||||||
|
|
||||||
background: hsl(var(--custom-color-hue),
|
--fc-event-bg-color: hsl(var(--custom-color-hue),
|
||||||
var(--calendar-coll-event-background-saturation),
|
var(--calendar-coll-event-background-saturation),
|
||||||
var(--calendar-coll-event-background-lightness)) !important;
|
var(--calendar-coll-event-background-lightness)) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -307,9 +307,11 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
|||||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||||
}
|
}
|
||||||
|
|
||||||
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
e.el.addEventListener("contextmenu", async (contextMenuEvent) => {
|
||||||
const noteId = e.event.extendedProps.noteId;
|
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||||
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
if (!note) return;
|
||||||
|
|
||||||
|
openCalendarContextMenu(contextMenuEvent, note, parentNote);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return { eventDidMount };
|
return { eventDidMount };
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
"better-sqlite3": "12.5.0",
|
"better-sqlite3": "12.5.0",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.21.0",
|
||||||
"yargs": "18.0.0"
|
"yargs": "18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ test("User can change language from settings", async ({ page, context }) => {
|
|||||||
// Check that the default value (English) is set.
|
// Check that the default value (English) is set.
|
||||||
await expect(app.currentNoteSplit).toContainText("First day of the week");
|
await expect(app.currentNoteSplit).toContainText("First day of the week");
|
||||||
const languageCombobox = app.dropdown(app.currentNoteSplit.locator(".options-section .dropdown").first());
|
const languageCombobox = app.dropdown(app.currentNoteSplit.locator(".options-section .dropdown").first());
|
||||||
await expect(languageCombobox).toContainText("English");
|
await expect(languageCombobox).toContainText("English (United States)");
|
||||||
|
|
||||||
// Select Chinese and ensure the translation is set.
|
// Select Chinese and ensure the translation is set.
|
||||||
await languageCombobox.selectOptionByText("简体中文");
|
await languageCombobox.selectOptionByText("简体中文");
|
||||||
@ -53,8 +53,8 @@ test("User can change language from settings", async ({ page, context }) => {
|
|||||||
await expect(languageCombobox).toContainText("简体中文");
|
await expect(languageCombobox).toContainText("简体中文");
|
||||||
|
|
||||||
// Select English again.
|
// Select English again.
|
||||||
await languageCombobox.selectOptionByText("English");
|
await languageCombobox.selectOptionByText("English (United States)");
|
||||||
await app.currentNoteSplit.locator("button[name=restart-app-button]").click();
|
await app.currentNoteSplit.locator("button[name=restart-app-button]").click();
|
||||||
await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 });
|
await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 });
|
||||||
await expect(languageCombobox).toContainText("English");
|
await expect(languageCombobox).toContainText("English (United States)");
|
||||||
});
|
});
|
||||||
|
|||||||
55
docs/README-fa.md
vendored
55
docs/README-fa.md
vendored
@ -33,50 +33,39 @@ quick overview:
|
|||||||
|
|
||||||
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
||||||
|
|
||||||
## ⏬ Download
|
## ⏬ دانلود
|
||||||
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) –
|
- [آخرین انتشار]{1} – نسخه پایدار، برای بیشتر کاربران پیشنهاد میشود.
|
||||||
stable version, recommended for most users.
|
|
||||||
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) –
|
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) –
|
||||||
unstable development version, updated daily with the latest features and
|
unstable development version, updated daily with the latest features and
|
||||||
fixes.
|
fixes.
|
||||||
|
|
||||||
## 📚 Documentation
|
## 📚 کتابچه راهنما
|
||||||
|
|
||||||
**Visit our comprehensive documentation at
|
**Visit our comprehensive documentation at
|
||||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||||
|
|
||||||
Our documentation is available in multiple formats:
|
مستندات ما در چندین قالب مختلف در دسترس است:
|
||||||
- **Online Documentation**: Browse the full documentation at
|
- مستندات آنلاین: میتوانید نسخهٔ کامل مستندات را در
|
||||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
|
[docs.triliumnotes.org](https://docs.triliumnotes.org/) مرور کنید
|
||||||
- **In-App Help**: Press `F1` within Trilium to access the same documentation
|
- ** In-App Help **: Press `F1 ` در Trilium برای دسترسی به همان اسناد به طور
|
||||||
directly in the application
|
مستقیم در برنامه
|
||||||
- **GitHub**: Navigate through the [User
|
- ** GitHub **: از طریق [راهنمای کاربر] در این مخزن حرکت کنید
|
||||||
Guide](./docs/User%20Guide/User%20Guide/) in this repository
|
|
||||||
|
|
||||||
### Quick Links
|
### لینکهای سریع
|
||||||
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
- راهنمای شروع کار
|
||||||
- [Installation
|
- دستورالعملهای نصب
|
||||||
Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
- راهاندازی داکر
|
||||||
- [Docker
|
- ارتقای TriliumNext
|
||||||
Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
- مفاهیم و ویژگیهای پایه
|
||||||
- [Upgrading
|
- الگوهای پایگاه دانشی شخصی
|
||||||
TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
|
||||||
- [Basic Concepts and
|
|
||||||
Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
|
||||||
- [Patterns of Personal Knowledge
|
|
||||||
Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
|
|
||||||
|
|
||||||
## 🎁 Features
|
## 🎁 ویژگیها
|
||||||
|
|
||||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed
|
* یادداشتها میتوانند در یک درخت با عمق دلخواه سازماندهی شوند. یک یادداشت
|
||||||
into multiple places in the tree (see
|
میتواند در چندین نقطهٔ مختلف از درخت قرار گیرد.
|
||||||
[cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
|
* ویرایشگر یادداشت غنی WYSIWYG از جمله جداول، تصاویر و [math] [1] با علامت گذاری
|
||||||
* Rich WYSIWYG note editor including e.g. tables, images and
|
[autoformat] [2]
|
||||||
[math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown
|
* پشتیبانی از ویرایش [یادداشت با کد منبع][۱]، از جمله نحو برجسته
|
||||||
[autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
|
|
||||||
* Support for editing [notes with source
|
|
||||||
code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax
|
|
||||||
highlighting
|
|
||||||
* Fast and easy [navigation between
|
* Fast and easy [navigation between
|
||||||
notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text
|
notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text
|
||||||
search and [note
|
search and [note
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
"esbuild": "0.27.0",
|
"esbuild": "0.27.0",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-playwright": "2.3.0",
|
"eslint-plugin-playwright": "2.4.0",
|
||||||
"eslint-plugin-react-hooks": "7.0.1",
|
"eslint-plugin-react-hooks": "7.0.1",
|
||||||
"happy-dom": "~20.0.0",
|
"happy-dom": "~20.0.0",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
@ -62,7 +62,7 @@
|
|||||||
"react-refresh": "0.18.0",
|
"react-refresh": "0.18.0",
|
||||||
"rollup-plugin-webpack-stats": "2.1.8",
|
"rollup-plugin-webpack-stats": "2.1.8",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.21.0",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.0",
|
||||||
"typescript-eslint": "8.48.0",
|
"typescript-eslint": "8.48.0",
|
||||||
"upath": "2.0.1",
|
"upath": "2.0.1",
|
||||||
|
|||||||
@ -71,6 +71,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ckeditor/ckeditor5-icons": "47.2.0"
|
"@ckeditor/ckeditor5-icons": "47.2.0",
|
||||||
|
"mathlive": "0.108.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import ckeditor from './../theme/icons/math.svg?raw';
|
import ckeditor from './../theme/icons/math.svg?raw';
|
||||||
import './augmentation.js';
|
import './augmentation.js';
|
||||||
import "../theme/mathform.css";
|
import "../theme/mathform.css";
|
||||||
|
import 'mathlive';
|
||||||
|
import 'mathlive/fonts.css';
|
||||||
|
import 'mathlive/static.css';
|
||||||
|
|
||||||
export { default as Math } from './math.js';
|
export { default as Math } from './math.js';
|
||||||
export { default as MathUI } from './mathui.js';
|
export { default as MathUI } from './mathui.js';
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export default class MathUI extends Plugin {
|
|||||||
this._balloon.showStack( 'main' );
|
this._balloon.showStack( 'main' );
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.formView?.mathInputView.fieldView.element?.focus();
|
this.formView?.mathLiveInputView.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,31 +71,38 @@ export default class MathUI extends Plugin {
|
|||||||
throw new CKEditorError( 'math-command' );
|
throw new CKEditorError( 'math-command' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const mathConfig = editor.config.get( 'math' )!;
|
const mathConfig = editor.config.get( 'math' )!;
|
||||||
|
|
||||||
const formView = new MainFormView(
|
const formView = new MainFormView(
|
||||||
editor.locale,
|
editor.locale,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
{
|
||||||
mathConfig.engine!,
|
engine: mathConfig.engine!,
|
||||||
mathConfig.lazyLoad,
|
lazyLoad: mathConfig.lazyLoad,
|
||||||
|
previewUid: this._previewUid,
|
||||||
|
previewClassName: mathConfig.previewClassName!,
|
||||||
|
katexRenderOptions: mathConfig.katexRenderOptions!
|
||||||
|
},
|
||||||
mathConfig.enablePreview,
|
mathConfig.enablePreview,
|
||||||
this._previewUid,
|
mathConfig.popupClassName!
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
mathConfig.previewClassName!,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
mathConfig.popupClassName!,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
mathConfig.katexRenderOptions!
|
|
||||||
);
|
);
|
||||||
|
|
||||||
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
|
formView.mathLiveInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||||
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
|
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
|
||||||
|
|
||||||
// Form elements should be read-only when corresponding commands are disabled.
|
// Form elements should be read-only when corresponding commands are disabled.
|
||||||
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
|
formView.mathLiveInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
|
||||||
formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
|
formView.saveButtonView.bind( 'isEnabled' ).to(
|
||||||
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
|
mathCommand,
|
||||||
|
'isEnabled',
|
||||||
|
formView.mathLiveInputView,
|
||||||
|
'value',
|
||||||
|
( commandEnabled, equation ) => {
|
||||||
|
const normalizedEquation = ( equation ?? '' ).trim();
|
||||||
|
return commandEnabled && normalizedEquation.length > 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
|
||||||
|
|
||||||
// Listen to submit button click
|
// Listen to submit button click
|
||||||
this.listenTo( formView, 'submit', () => {
|
this.listenTo( formView, 'submit', () => {
|
||||||
@ -122,18 +129,6 @@ export default class MathUI extends Plugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Allow the textarea to be resizable
|
|
||||||
formView.mathInputView.fieldView.once('render', () => {
|
|
||||||
const textarea = formView.mathInputView.fieldView.element;
|
|
||||||
if (!textarea) return;
|
|
||||||
Object.assign(textarea.style, {
|
|
||||||
resize: 'both',
|
|
||||||
height: '100px',
|
|
||||||
width: '400px',
|
|
||||||
minWidth: '100%',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return formView;
|
return formView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,14 +157,14 @@ export default class MathUI extends Plugin {
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
if ( this._balloon.visibleView === this.formView ) {
|
if ( this._balloon.visibleView === this.formView ) {
|
||||||
this.formView.mathInputView.fieldView.element?.select();
|
this.formView.mathLiveInputView.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show preview element
|
// Show preview element
|
||||||
const previewEl = document.getElementById( this._previewUid );
|
const previewEl = document.getElementById( this._previewUid );
|
||||||
if ( previewEl && this.formView.previewEnabled ) {
|
if ( previewEl && this.formView.mathView ) {
|
||||||
// Force refresh preview
|
// Force refresh preview
|
||||||
this.formView.mathView?.updateMath();
|
this.formView.mathView.updateMath();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formView.equation = mathCommand.value ?? '';
|
this.formView.equation = mathCommand.value ?? '';
|
||||||
|
|||||||
@ -1,270 +1,219 @@
|
|||||||
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
|
import {
|
||||||
import IconCheck from "@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw";
|
ButtonView,
|
||||||
import IconCancel from "@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw";
|
FocusCycler,
|
||||||
|
LabelView,
|
||||||
|
submitHandler,
|
||||||
|
SwitchButtonView,
|
||||||
|
View,
|
||||||
|
ViewCollection,
|
||||||
|
type FocusableView,
|
||||||
|
Locale,
|
||||||
|
FocusTracker,
|
||||||
|
KeystrokeHandler
|
||||||
|
} from 'ckeditor5';
|
||||||
|
import IconCheck from '@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw';
|
||||||
|
import IconCancel from '@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw';
|
||||||
import { extractDelimiters, hasDelimiters } from '../utils.js';
|
import { extractDelimiters, hasDelimiters } from '../utils.js';
|
||||||
import MathView from './mathview.js';
|
import MathView, { type MathViewOptions } from './mathview.js';
|
||||||
|
import MathLiveInputView from './mathliveinputview.js';
|
||||||
|
import RawLatexInputView from './rawlatexinputview.js';
|
||||||
import '../../theme/mathform.css';
|
import '../../theme/mathform.css';
|
||||||
import type { KatexOptions } from '../typings-external.js';
|
|
||||||
|
|
||||||
class MathInputView extends LabeledFieldView<TextareaView> {
|
|
||||||
public value: null | string = null;
|
|
||||||
public isReadOnly = false;
|
|
||||||
|
|
||||||
constructor( locale: Locale ) {
|
|
||||||
super( locale, createLabeledTextarea );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MainFormView extends View {
|
export default class MainFormView extends View {
|
||||||
public saveButtonView: ButtonView;
|
public saveButtonView: ButtonView;
|
||||||
public mathInputView: MathInputView;
|
|
||||||
public displayButtonView: SwitchButtonView;
|
|
||||||
public cancelButtonView: ButtonView;
|
public cancelButtonView: ButtonView;
|
||||||
public previewEnabled: boolean;
|
public displayButtonView: SwitchButtonView;
|
||||||
public previewLabel?: LabelView;
|
|
||||||
|
public mathLiveInputView: MathLiveInputView;
|
||||||
|
public rawLatexInputView: RawLatexInputView;
|
||||||
public mathView?: MathView;
|
public mathView?: MathView;
|
||||||
public override locale: Locale = new Locale();
|
|
||||||
public lazyLoad: undefined | ( () => Promise<void> );
|
public focusTracker = new FocusTracker();
|
||||||
|
public keystrokes = new KeystrokeHandler();
|
||||||
|
private _focusables = new ViewCollection<FocusableView>();
|
||||||
|
private _focusCycler: FocusCycler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
engine:
|
mathViewOptions: MathViewOptions,
|
||||||
| 'mathjax'
|
|
||||||
| 'katex'
|
|
||||||
| ( (
|
|
||||||
equation: string,
|
|
||||||
element: HTMLElement,
|
|
||||||
display: boolean,
|
|
||||||
) => void ),
|
|
||||||
lazyLoad: undefined | ( () => Promise<void> ),
|
|
||||||
previewEnabled = false,
|
previewEnabled = false,
|
||||||
previewUid: string,
|
popupClassName: string[] = []
|
||||||
previewClassName: Array<string>,
|
|
||||||
popupClassName: Array<string>,
|
|
||||||
katexRenderOptions: KatexOptions
|
|
||||||
) {
|
) {
|
||||||
super( locale );
|
super( locale );
|
||||||
|
|
||||||
const t = locale.t;
|
const t = locale.t;
|
||||||
|
|
||||||
// Submit button
|
// --- 1. View Initialization ---
|
||||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
|
|
||||||
|
this.mathLiveInputView = new MathLiveInputView( locale );
|
||||||
|
this.rawLatexInputView = new RawLatexInputView( locale );
|
||||||
|
this.rawLatexInputView.label = t( 'LaTeX' );
|
||||||
|
|
||||||
|
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save' );
|
||||||
this.saveButtonView.type = 'submit';
|
this.saveButtonView.type = 'submit';
|
||||||
|
|
||||||
// Equation input
|
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
|
||||||
this.mathInputView = this._createMathInput();
|
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
|
||||||
|
|
||||||
// Display button
|
this.displayButtonView = this._createDisplayButton( t );
|
||||||
this.displayButtonView = this._createDisplayButton();
|
|
||||||
|
|
||||||
// Cancel button
|
// --- 2. Construct Children & Preview ---
|
||||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
|
|
||||||
|
|
||||||
this.previewEnabled = previewEnabled;
|
const children: View[] = [
|
||||||
|
this.mathLiveInputView,
|
||||||
let children = [];
|
this.rawLatexInputView,
|
||||||
if ( this.previewEnabled ) {
|
|
||||||
// Preview label
|
|
||||||
this.previewLabel = new LabelView( locale );
|
|
||||||
this.previewLabel.text = t( 'Equation preview' );
|
|
||||||
|
|
||||||
// Math element
|
|
||||||
this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions );
|
|
||||||
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
|
||||||
|
|
||||||
children = [
|
|
||||||
this.mathInputView,
|
|
||||||
this.displayButtonView,
|
|
||||||
this.previewLabel,
|
|
||||||
this.mathView
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
children = [
|
|
||||||
this.mathInputView,
|
|
||||||
this.displayButtonView
|
this.displayButtonView
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ( previewEnabled ) {
|
||||||
|
const previewLabel = new LabelView( locale );
|
||||||
|
previewLabel.text = t( 'Equation preview' );
|
||||||
|
|
||||||
|
// Clean instantiation using the options object
|
||||||
|
this.mathView = new MathView( locale, mathViewOptions );
|
||||||
|
|
||||||
|
// Bind display mode: When button flips, preview updates automatically
|
||||||
|
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
||||||
|
|
||||||
|
children.push( previewLabel, this.mathView );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add UI elements to template
|
// --- 3. Sync Logic ---
|
||||||
|
this._setupInputSync( previewEnabled );
|
||||||
|
|
||||||
|
// --- 4. Template Setup ---
|
||||||
this.setTemplate( {
|
this.setTemplate( {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
attributes: {
|
attributes: {
|
||||||
class: [
|
class: [ 'ck', 'ck-math-form', ...popupClassName ],
|
||||||
'ck',
|
|
||||||
'ck-math-form',
|
|
||||||
...popupClassName
|
|
||||||
],
|
|
||||||
tabindex: '-1',
|
tabindex: '-1',
|
||||||
spellcheck: 'false'
|
spellcheck: 'false'
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
attributes: {
|
attributes: { class: [ 'ck-math-scroll' ] },
|
||||||
class: [
|
children: [ { tag: 'div', attributes: { class: [ 'ck-math-view' ] }, children } ]
|
||||||
'ck-math-view'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
children
|
{
|
||||||
},
|
tag: 'div',
|
||||||
this.saveButtonView,
|
attributes: { class: [ 'ck-math-button-row' ] },
|
||||||
this.cancelButtonView
|
children: [ this.saveButtonView, this.cancelButtonView ]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// --- 5. Accessibility ---
|
||||||
|
this._focusCycler = new FocusCycler( {
|
||||||
|
focusables: this._focusables,
|
||||||
|
focusTracker: this.focusTracker,
|
||||||
|
keystrokeHandler: this.keystrokes,
|
||||||
|
actions: { focusPrevious: 'shift + tab', focusNext: 'tab' }
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): void {
|
public override render(): void {
|
||||||
super.render();
|
super.render();
|
||||||
|
|
||||||
// Prevent default form submit event & trigger custom 'submit'
|
submitHandler( { view: this } );
|
||||||
submitHandler( {
|
|
||||||
view: this
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Register form elements to focusable elements
|
// Register focusables
|
||||||
const childViews = [
|
[
|
||||||
this.mathInputView,
|
this.mathLiveInputView,
|
||||||
|
this.rawLatexInputView,
|
||||||
this.displayButtonView,
|
this.displayButtonView,
|
||||||
this.saveButtonView,
|
this.saveButtonView,
|
||||||
this.cancelButtonView
|
this.cancelButtonView
|
||||||
];
|
].forEach( v => {
|
||||||
|
|
||||||
childViews.forEach( v => {
|
|
||||||
if ( v.element ) {
|
if ( v.element ) {
|
||||||
this._focusables.add( v );
|
this._focusables.add( v );
|
||||||
this.focusTracker.add( v.element );
|
this.focusTracker.add( v.element );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Listen to keypresses inside form element
|
if ( this.element ) this.keystrokes.listenTo( this.element );
|
||||||
if ( this.element ) {
|
|
||||||
this.keystrokes.listenTo( this.element );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get equation(): string {
|
||||||
|
return this.mathLiveInputView.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public set equation( equation: string ) {
|
||||||
|
const norm = equation.trim();
|
||||||
|
// Direct updates to the "source of truth"
|
||||||
|
this.mathLiveInputView.value = norm.length ? norm : null;
|
||||||
|
this.rawLatexInputView.value = norm;
|
||||||
|
if ( this.mathView ) this.mathView.value = norm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus(): void {
|
public focus(): void {
|
||||||
this._focusCycler.focusFirst();
|
this._focusCycler.focusFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get equation(): string {
|
/**
|
||||||
return this.mathInputView.fieldView.element?.value ?? '';
|
* Sets up split handlers for synchronization.
|
||||||
}
|
*/
|
||||||
|
private _setupInputSync( previewEnabled: boolean ): void {
|
||||||
|
// Handler 1: MathLive -> Raw LaTeX
|
||||||
|
this.mathLiveInputView.on( 'change:value', () => {
|
||||||
|
let eq = ( this.mathLiveInputView.value ?? '' ).trim();
|
||||||
|
|
||||||
public set equation( equation: string ) {
|
// Delimiter Normalization
|
||||||
if ( this.mathInputView.fieldView.element ) {
|
if ( hasDelimiters( eq ) ) {
|
||||||
this.mathInputView.fieldView.element.value = equation;
|
const params = extractDelimiters( eq );
|
||||||
}
|
eq = params.equation;
|
||||||
if ( this.previewEnabled && this.mathView ) {
|
|
||||||
this.mathView.value = equation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public focusTracker: FocusTracker = new FocusTracker();
|
|
||||||
public keystrokes: KeystrokeHandler = new KeystrokeHandler();
|
|
||||||
private _focusables = new ViewCollection<FocusableView>();
|
|
||||||
private _focusCycler: FocusCycler = new FocusCycler( {
|
|
||||||
focusables: this._focusables,
|
|
||||||
focusTracker: this.focusTracker,
|
|
||||||
keystrokeHandler: this.keystrokes,
|
|
||||||
actions: {
|
|
||||||
focusPrevious: 'shift + tab',
|
|
||||||
focusNext: 'tab'
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
private _createMathInput() {
|
|
||||||
const t = this.locale.t;
|
|
||||||
|
|
||||||
// Create equation input
|
|
||||||
const mathInput = new MathInputView( this.locale );
|
|
||||||
const fieldView = mathInput.fieldView;
|
|
||||||
mathInput.infoText = t( 'Insert equation in TeX format.' );
|
|
||||||
|
|
||||||
const onInput = () => {
|
|
||||||
if ( fieldView.element != null ) {
|
|
||||||
let equationInput = fieldView.element.value.trim();
|
|
||||||
|
|
||||||
// If input has delimiters
|
|
||||||
if ( hasDelimiters( equationInput ) ) {
|
|
||||||
// Get equation without delimiters
|
|
||||||
const params = extractDelimiters( equationInput );
|
|
||||||
|
|
||||||
// Remove delimiters from input field
|
|
||||||
fieldView.element.value = params.equation;
|
|
||||||
|
|
||||||
equationInput = params.equation;
|
|
||||||
|
|
||||||
// update display button and preview
|
|
||||||
this.displayButtonView.isOn = params.display;
|
this.displayButtonView.isOn = params.display;
|
||||||
|
|
||||||
|
// UX Fix: If we stripped delimiters, update the source
|
||||||
|
// so the visual editor doesn't show them.
|
||||||
|
if ( this.mathLiveInputView.value !== eq ) {
|
||||||
|
this.mathLiveInputView.value = eq;
|
||||||
}
|
}
|
||||||
if ( this.previewEnabled && this.mathView ) {
|
|
||||||
// Update preview view
|
|
||||||
this.mathView.value = equationInput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveButtonView.isEnabled = !!equationInput;
|
// Sync to Raw LaTeX
|
||||||
}
|
if ( this.rawLatexInputView.value !== eq ) {
|
||||||
};
|
this.rawLatexInputView.value = eq;
|
||||||
|
|
||||||
fieldView.on( 'render', onInput );
|
|
||||||
fieldView.on( 'input', onInput );
|
|
||||||
|
|
||||||
return mathInput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createButton(
|
// Sync to Preview
|
||||||
label: string,
|
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||||
icon: string,
|
this.mathView.value = eq;
|
||||||
className: string,
|
|
||||||
eventName: string | null
|
|
||||||
) {
|
|
||||||
const button = new ButtonView( this.locale );
|
|
||||||
|
|
||||||
button.set( {
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
tooltip: true
|
|
||||||
} );
|
|
||||||
|
|
||||||
button.extendTemplate( {
|
|
||||||
attributes: {
|
|
||||||
class: className
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if ( eventName ) {
|
// Handler 2: Raw LaTeX -> MathLive
|
||||||
button.delegate( 'execute' ).to( this, eventName );
|
this.rawLatexInputView.on( 'change:value', () => {
|
||||||
|
const eq = ( this.rawLatexInputView.value ?? '' ).trim();
|
||||||
|
const normalized = eq.length ? eq : null;
|
||||||
|
|
||||||
|
// Sync to MathLive
|
||||||
|
if ( this.mathLiveInputView.value !== normalized ) {
|
||||||
|
this.mathLiveInputView.value = normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
return button;
|
// Sync to Preview
|
||||||
}
|
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||||
|
this.mathView.value = eq;
|
||||||
private _createDisplayButton() {
|
|
||||||
const t = this.locale.t;
|
|
||||||
|
|
||||||
const switchButton = new SwitchButtonView( this.locale );
|
|
||||||
|
|
||||||
switchButton.set( {
|
|
||||||
label: t( 'Display mode' ),
|
|
||||||
withText: true
|
|
||||||
} );
|
|
||||||
|
|
||||||
switchButton.extendTemplate( {
|
|
||||||
attributes: {
|
|
||||||
class: 'ck-button-display-toggle'
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
switchButton.on( 'execute', () => {
|
|
||||||
// Toggle state
|
|
||||||
switchButton.isOn = !switchButton.isOn;
|
|
||||||
|
|
||||||
if ( this.previewEnabled && this.mathView ) {
|
|
||||||
// Update preview view
|
|
||||||
this.mathView.display = switchButton.isOn;
|
|
||||||
}
|
}
|
||||||
} );
|
|
||||||
|
|
||||||
return switchButton;
|
private _createButton( label: string, icon: string, className: string ): ButtonView {
|
||||||
|
const btn = new ButtonView( this.locale );
|
||||||
|
btn.set( { label, icon, tooltip: true } );
|
||||||
|
btn.extendTemplate( { attributes: { class: className } } );
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
|
||||||
|
const btn = new SwitchButtonView( this.locale );
|
||||||
|
btn.set( { label: t( 'Display mode' ), withText: true } );
|
||||||
|
btn.extendTemplate( { attributes: { class: 'ck-button-display-toggle' } } );
|
||||||
|
|
||||||
|
btn.on( 'execute', () => {
|
||||||
|
btn.isOn = !btn.isOn;
|
||||||
|
// mathView updates automatically via bind()
|
||||||
|
} );
|
||||||
|
return btn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
packages/ckeditor5-math/src/ui/mathliveinputview.ts
Normal file
116
packages/ckeditor5-math/src/ui/mathliveinputview.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { View, type Locale } from 'ckeditor5';
|
||||||
|
import 'mathlive'; // Import side-effects only (registers the <math-field> tag)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing the custom <math-field> element.
|
||||||
|
*/
|
||||||
|
interface MathFieldElement extends HTMLElement {
|
||||||
|
value: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
mathVirtualKeyboardPolicy: string;
|
||||||
|
// Interface includes the shortcuts property
|
||||||
|
inlineShortcuts: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the MathLive <math-field> component.
|
||||||
|
*/
|
||||||
|
export default class MathLiveInputView extends View {
|
||||||
|
/**
|
||||||
|
* The current LaTeX value.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
public declare value: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only state.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
public declare isReadOnly: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the DOM element.
|
||||||
|
* Typed as MathFieldElement | null for proper TS support.
|
||||||
|
*/
|
||||||
|
public mathfield: MathFieldElement | null = null;
|
||||||
|
|
||||||
|
constructor( locale: Locale ) {
|
||||||
|
super( locale );
|
||||||
|
|
||||||
|
this.set( 'value', null );
|
||||||
|
this.set( 'isReadOnly', false );
|
||||||
|
|
||||||
|
this.setTemplate( {
|
||||||
|
tag: 'div',
|
||||||
|
attributes: {
|
||||||
|
class: [ 'ck', 'ck-mathlive-input' ]
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
public override render(): void {
|
||||||
|
super.render();
|
||||||
|
|
||||||
|
// 1. Create element with the specific type
|
||||||
|
const mathfield = document.createElement( 'math-field' ) as MathFieldElement;
|
||||||
|
|
||||||
|
// 2. Configure Options
|
||||||
|
mathfield.mathVirtualKeyboardPolicy = 'manual';
|
||||||
|
|
||||||
|
//Disable differential D
|
||||||
|
mathfield.addEventListener( 'mount', () => {
|
||||||
|
mathfield.inlineShortcuts = {
|
||||||
|
...mathfield.inlineShortcuts, // Safe to read now
|
||||||
|
dx: 'dx',
|
||||||
|
dy: 'dy',
|
||||||
|
dt: 'dt'
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
// Disable sounds safely
|
||||||
|
const MathfieldConstructor = customElements.get( 'math-field' );
|
||||||
|
if ( MathfieldConstructor ) {
|
||||||
|
const proto = MathfieldConstructor as any;
|
||||||
|
if ( proto.soundsDirectory !== null ) proto.soundsDirectory = null;
|
||||||
|
if ( proto.plonkSound !== null ) proto.plonkSound = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Set Initial State
|
||||||
|
mathfield.value = this.value ?? '';
|
||||||
|
mathfield.readOnly = this.isReadOnly;
|
||||||
|
|
||||||
|
// 4. Bind Events (DOM -> Observable)
|
||||||
|
mathfield.addEventListener( 'input', () => {
|
||||||
|
const val = mathfield.value;
|
||||||
|
this.value = val.length ? val : null;
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 5. Bind Events (Observable -> DOM)
|
||||||
|
this.on( 'change:value', ( _evt, _name, nextValue ) => {
|
||||||
|
if ( mathfield.value !== nextValue ) {
|
||||||
|
mathfield.value = nextValue ?? '';
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => {
|
||||||
|
mathfield.readOnly = nextValue;
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 6. Mount to the wrapper view
|
||||||
|
this.element?.appendChild( mathfield );
|
||||||
|
this.mathfield = mathfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
public focus(): void {
|
||||||
|
this.mathfield?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override destroy(): void {
|
||||||
|
if ( this.mathfield ) {
|
||||||
|
this.mathfield.remove();
|
||||||
|
this.mathfield = null;
|
||||||
|
}
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
|
|||||||
import type { KatexOptions } from '../typings-external.js';
|
import type { KatexOptions } from '../typings-external.js';
|
||||||
import { renderEquation } from '../utils.js';
|
import { renderEquation } from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the MathView.
|
||||||
|
*/
|
||||||
|
export interface MathViewOptions {
|
||||||
|
engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
||||||
|
lazyLoad: undefined | ( () => Promise<void> );
|
||||||
|
previewUid: string;
|
||||||
|
previewClassName: Array<string>;
|
||||||
|
katexRenderOptions: KatexOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export default class MathView extends View {
|
export default class MathView extends View {
|
||||||
|
/**
|
||||||
|
* The LaTeX equation value to render.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
public declare value: string;
|
public declare value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to render in display mode (centered) or inline.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
public declare display: boolean;
|
public declare display: boolean;
|
||||||
public previewUid: string;
|
|
||||||
public previewClassName: Array<string>;
|
|
||||||
public katexRenderOptions: KatexOptions;
|
|
||||||
public engine:
|
|
||||||
| 'mathjax'
|
|
||||||
| 'katex'
|
|
||||||
| ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
|
||||||
public lazyLoad: undefined | ( () => Promise<void> );
|
|
||||||
|
|
||||||
constructor(
|
/**
|
||||||
engine:
|
* Configuration options passed during initialization.
|
||||||
| 'mathjax'
|
*/
|
||||||
| 'katex'
|
private options: MathViewOptions;
|
||||||
| ( (
|
|
||||||
equation: string,
|
constructor( locale: Locale, options: MathViewOptions ) {
|
||||||
element: HTMLElement,
|
|
||||||
display: boolean,
|
|
||||||
) => void ),
|
|
||||||
lazyLoad: undefined | ( () => Promise<void> ),
|
|
||||||
locale: Locale,
|
|
||||||
previewUid: string,
|
|
||||||
previewClassName: Array<string>,
|
|
||||||
katexRenderOptions: KatexOptions
|
|
||||||
) {
|
|
||||||
super( locale );
|
super( locale );
|
||||||
|
this.options = options;
|
||||||
this.engine = engine;
|
|
||||||
this.lazyLoad = lazyLoad;
|
|
||||||
this.previewUid = previewUid;
|
|
||||||
this.katexRenderOptions = katexRenderOptions;
|
|
||||||
this.previewClassName = previewClassName;
|
|
||||||
|
|
||||||
this.set( 'value', '' );
|
this.set( 'value', '' );
|
||||||
this.set( 'display', false );
|
this.set( 'display', false );
|
||||||
|
|
||||||
|
// Update rendering when state changes.
|
||||||
|
// Checking isRendered prevents errors during initialization.
|
||||||
this.on( 'change', () => {
|
this.on( 'change', () => {
|
||||||
if ( this.isRendered ) {
|
if ( this.isRendered ) {
|
||||||
this.updateMath();
|
this.updateMath();
|
||||||
@ -56,16 +56,20 @@ export default class MathView extends View {
|
|||||||
|
|
||||||
public updateMath(): void {
|
public updateMath(): void {
|
||||||
if ( this.element ) {
|
if ( this.element ) {
|
||||||
|
|
||||||
|
// This prevents the new render from appending to the old one.
|
||||||
|
this.element.textContent = '';
|
||||||
|
|
||||||
void renderEquation(
|
void renderEquation(
|
||||||
this.value,
|
this.value,
|
||||||
this.element,
|
this.element,
|
||||||
this.engine,
|
this.options.engine,
|
||||||
this.lazyLoad,
|
this.options.lazyLoad,
|
||||||
this.display,
|
this.display,
|
||||||
true,
|
true, // isPreview
|
||||||
this.previewUid,
|
this.options.previewUid,
|
||||||
this.previewClassName,
|
this.options.previewClassName,
|
||||||
this.katexRenderOptions
|
this.options.katexRenderOptions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
packages/ckeditor5-math/src/ui/rawlatexinputview.ts
Normal file
54
packages/ckeditor5-math/src/ui/rawlatexinputview.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView } from 'ckeditor5';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A labeled textarea view for direct LaTeX code editing.
|
||||||
|
*/
|
||||||
|
export default class RawLatexInputView extends LabeledFieldView<TextareaView> {
|
||||||
|
/**
|
||||||
|
* The current LaTeX value.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
public declare value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the input is in read-only mode.
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
public declare isReadOnly: boolean;
|
||||||
|
|
||||||
|
constructor( locale: Locale ) {
|
||||||
|
super( locale, createLabeledTextarea );
|
||||||
|
|
||||||
|
this.set( 'value', '' );
|
||||||
|
this.set( 'isReadOnly', false );
|
||||||
|
|
||||||
|
const fieldView = this.fieldView;
|
||||||
|
|
||||||
|
// 1. Sync: DOM (Textarea) -> Observable
|
||||||
|
fieldView.on( 'input', () => {
|
||||||
|
// We cast strictly to HTMLTextAreaElement to access '.value' safely
|
||||||
|
const textarea = fieldView.element as HTMLTextAreaElement;
|
||||||
|
if ( textarea ) {
|
||||||
|
this.value = textarea.value;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 2. Sync: Observable -> DOM (Textarea)
|
||||||
|
this.on( 'change:value', () => {
|
||||||
|
const textarea = fieldView.element as HTMLTextAreaElement;
|
||||||
|
// Check for difference to avoid cursor jumping
|
||||||
|
if ( textarea && textarea.value !== this.value ) {
|
||||||
|
textarea.value = this.value;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 3. Sync: ReadOnly State
|
||||||
|
this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => {
|
||||||
|
fieldView.isReadOnly = nextValue;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
public override render(): void {
|
||||||
|
super.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -168,13 +168,13 @@ describe( 'MathUI', () => {
|
|||||||
|
|
||||||
command.isEnabled = true;
|
command.isEnabled = true;
|
||||||
|
|
||||||
expect( formView!.mathInputView.isReadOnly ).to.be.false;
|
expect( formView!.mathLiveInputView.isReadOnly ).to.be.false;
|
||||||
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
||||||
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
||||||
|
|
||||||
command.isEnabled = false;
|
command.isEnabled = false;
|
||||||
|
|
||||||
expect( formView!.mathInputView.isReadOnly ).to.be.true;
|
expect( formView!.mathLiveInputView.isReadOnly ).to.be.true;
|
||||||
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
||||||
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
||||||
} );
|
} );
|
||||||
@ -407,22 +407,30 @@ describe( 'MathUI', () => {
|
|||||||
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
|
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
|
it( 'should bind mainFormView.mathLiveInputView#value to math command value', () => {
|
||||||
const command = editor.commands.get( 'math' );
|
const command = editor.commands.get( 'math' );
|
||||||
|
|
||||||
expect( formView!.mathInputView.value ).to.null;
|
expect( formView!.mathLiveInputView.value ).to.be.null;
|
||||||
|
|
||||||
command!.value = 'x^2';
|
command!.value = 'x^2';
|
||||||
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
|
expect( formView!.mathLiveInputView.value ).to.equal( 'x^2' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should execute math command on mainFormView#submit event', () => {
|
it( 'should execute math command on mainFormView#submit event', () => {
|
||||||
const executeSpy = vi.spyOn( editor, 'execute' );
|
const executeSpy = vi.spyOn( editor, 'execute' );
|
||||||
|
|
||||||
formView!.mathInputView.fieldView.element!.value = 'x^2';
|
formView!.mathLiveInputView.value = 'x^2';
|
||||||
formView!.fire( 'submit' );
|
formView!.fire( 'submit' );
|
||||||
|
|
||||||
expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
|
expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should sync mathLiveInputView and rawLatexInputView', () => {
|
||||||
|
formView!.mathLiveInputView.value = 'x^2';
|
||||||
|
expect( formView!.rawLatexInputView.value ).to.equal( 'x^2' );
|
||||||
|
|
||||||
|
formView!.rawLatexInputView.value = '\\frac{1}{2}';
|
||||||
|
expect( formView!.mathLiveInputView.value ).to.equal( '\\frac{1}{2}' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {
|
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {
|
||||||
|
|||||||
@ -1,35 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Math equation editor dialog styles
|
||||||
|
* Supports MathLive input, raw LaTeX textarea, and equation preview
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Main Dialog Container
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
.ck.ck-math-form {
|
.ck.ck-math-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
padding: var(--ck-spacing-standard);
|
padding: var(--ck-spacing-standard);
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 80vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
/* Mobile responsiveness */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.ck.ck-math-form {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
& .ck-math-view {
|
|
||||||
flex-basis: 100%;
|
|
||||||
|
|
||||||
& .ck-labeled-view {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .ck-label {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .ck-button {
|
|
||||||
flex-basis: 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-math-tex.ck-placeholder::before {
|
/* ============================================================================
|
||||||
|
Content Layout
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
.ck-math-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
gap: var(--ck-spacing-standard);
|
||||||
|
min-height: fit-content;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LaTeX section heading */
|
||||||
|
.ck-math-view > .ck-labeled-field-view::before {
|
||||||
|
content: "LaTeX";
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ck-color-text, #333);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-left: 2px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equation preview section heading */
|
||||||
|
.ck-math-view > math-field::before {
|
||||||
|
content: "Equation preview";
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ck-color-text, #333);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-left: 2px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add spacing between preview and action buttons */
|
||||||
|
.ck-math-view > math-field {
|
||||||
|
margin-bottom: var(--ck-spacing-large, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons row (Save/Cancel) */
|
||||||
|
.ck-math-button-row {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: var(--ck-spacing-standard);
|
||||||
|
margin-top: var(--ck-spacing-standard);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Shared Styles for Input Fields
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
/* Base styling for both MathLive fields and textareas */
|
||||||
|
.ck.ck-math-form math-field,
|
||||||
|
.ck.ck-math-form textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--ck-spacing-small);
|
||||||
|
background: var(--ck-color-input-background) !important;
|
||||||
|
color: var(--ck-color-input-text, inherit);
|
||||||
|
font-size: var(--ck-font-size-base);
|
||||||
|
border: none !important;
|
||||||
|
border-radius: var(--ck-border-radius, 6px);
|
||||||
|
outline: 3px solid transparent;
|
||||||
|
outline-offset: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MathLive-specific configuration */
|
||||||
|
.ck.ck-math-form math-field {
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
|
||||||
|
/* MathLive theme customization */
|
||||||
|
--selection-background-color: rgba(33, 150, 243, 0.2);
|
||||||
|
--selection-color: inherit;
|
||||||
|
--contains-highlight-background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
MathLive Visual Editor (Top Input)
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
.ck.ck-mathlive-input {
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: fit-content;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-bottom: var(--ck-spacing-small);
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure MathLive shadow DOM layout */
|
||||||
|
.ck.ck-math-form math-field::part(container),
|
||||||
|
.ck.ck-math-form math-field::part(content),
|
||||||
|
.ck.ck-math-form math-field::part(field) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position MathLive UI controls */
|
||||||
|
.ck.ck-math-form math-field::part(virtual-keyboard-toggle),
|
||||||
|
.ck.ck-math-form math-field::part(menu-toggle) {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-math-form math-field::part(virtual-keyboard-toggle) {
|
||||||
|
right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-math-form math-field::part(menu-toggle) {
|
||||||
|
right: 8px;
|
||||||
|
display: flex !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Raw LaTeX Textarea (Middle Input)
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
.ck-math-view .ck-labeled-field-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 65vh;
|
||||||
|
resize: both;
|
||||||
|
overflow: auto;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the default label (we use ::before for custom heading) */
|
||||||
|
.ck-math-view .ck-labeled-field-view .ck-label {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck.ck-toolbar-container {
|
/* Textarea wrapper */
|
||||||
z-index: calc(var(--ck-z-panel) + 2);
|
.ck-math-view .ck-labeled-field-view .ck-labeled-field-view__input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw LaTeX textarea styling */
|
||||||
|
.ck-math-view .ck-labeled-field-view textarea {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
resize: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Textarea hover and focus states */
|
||||||
|
.ck-math-view .ck-labeled-field-view textarea:hover,
|
||||||
|
.ck-math-view .ck-labeled-field-view textarea:focus {
|
||||||
|
background: var(--ck-color-input-background) !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export interface Locale {
|
|||||||
const UNSORTED_LOCALES = [
|
const UNSORTED_LOCALES = [
|
||||||
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" },
|
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" },
|
||||||
{ id: "de", name: "Deutsch", electronLocale: "de" },
|
{ id: "de", name: "Deutsch", electronLocale: "de" },
|
||||||
{ id: "en", name: "English", electronLocale: "en" },
|
{ id: "en", name: "English (United States)", electronLocale: "en" },
|
||||||
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB" },
|
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB" },
|
||||||
{ id: "es", name: "Español", electronLocale: "es" },
|
{ id: "es", name: "Español", electronLocale: "es" },
|
||||||
{ id: "fr", name: "Français", electronLocale: "fr" },
|
{ id: "fr", name: "Français", electronLocale: "fr" },
|
||||||
|
|||||||
466
pnpm-lock.yaml
generated
466
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user