@jant/core 0.6.14 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Editor, type JSONContent } from "@tiptap/core";
|
|
5
|
+
import { createMarkdownContentExtensions } from "../../../lib/markdown-manager.js";
|
|
6
|
+
import { TableControls } from "../table-controls.js";
|
|
7
|
+
import { DEFAULT_TABLE_CONTROL_LABELS } from "../table-control-labels.js";
|
|
8
|
+
|
|
9
|
+
const editors: Editor[] = [];
|
|
10
|
+
|
|
11
|
+
function requireElement<T extends globalThis.Element>(
|
|
12
|
+
element: T | null,
|
|
13
|
+
message: string,
|
|
14
|
+
): T {
|
|
15
|
+
if (!element) throw new Error(message);
|
|
16
|
+
return element;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createEditor(): Editor {
|
|
20
|
+
const element = document.createElement("div");
|
|
21
|
+
document.body.appendChild(element);
|
|
22
|
+
const editor = new Editor({
|
|
23
|
+
element,
|
|
24
|
+
extensions: [
|
|
25
|
+
...createMarkdownContentExtensions(),
|
|
26
|
+
TableControls.configure({ labels: DEFAULT_TABLE_CONTROL_LABELS }),
|
|
27
|
+
],
|
|
28
|
+
content: "<p></p>",
|
|
29
|
+
});
|
|
30
|
+
vi.spyOn(editor.view, "coordsAtPos").mockImplementation(() => ({
|
|
31
|
+
left: 80,
|
|
32
|
+
right: 100,
|
|
33
|
+
top: 120,
|
|
34
|
+
bottom: 140,
|
|
35
|
+
}));
|
|
36
|
+
editors.push(editor);
|
|
37
|
+
return editor;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tableJson(editor: Editor): JSONContent {
|
|
41
|
+
const table = editor.getJSON().content?.find((node) => node.type === "table");
|
|
42
|
+
if (!table) throw new Error("expected table JSON");
|
|
43
|
+
return table;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tableDimensions(editor: Editor): { rows: number; columns: number } {
|
|
47
|
+
const table = tableJson(editor);
|
|
48
|
+
const rows = table.content ?? [];
|
|
49
|
+
return {
|
|
50
|
+
rows: rows.length,
|
|
51
|
+
columns: rows[0]?.content?.length ?? 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cursorPositionInRow(editor: Editor, targetRow: number): number {
|
|
56
|
+
let row = -1;
|
|
57
|
+
let position: number | null = null;
|
|
58
|
+
editor.state.doc.descendants((node, pos) => {
|
|
59
|
+
if (node.type.name === "tableRow") row += 1;
|
|
60
|
+
if (
|
|
61
|
+
position === null &&
|
|
62
|
+
row === targetRow &&
|
|
63
|
+
(node.type.name === "tableCell" || node.type.name === "tableHeader")
|
|
64
|
+
) {
|
|
65
|
+
position = pos + 2;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return position === null;
|
|
69
|
+
});
|
|
70
|
+
if (position === null) throw new Error(`expected row ${targetRow}`);
|
|
71
|
+
return position;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function focusTable(editor: Editor, rows = 3, columns = 3) {
|
|
75
|
+
editor.commands.focus();
|
|
76
|
+
editor.commands.insertTable({
|
|
77
|
+
rows,
|
|
78
|
+
cols: columns,
|
|
79
|
+
withHeaderRow: true,
|
|
80
|
+
});
|
|
81
|
+
await vi.waitFor(() => {
|
|
82
|
+
expect(
|
|
83
|
+
document.querySelector<HTMLElement>(".tiptap-table-controls")?.style
|
|
84
|
+
.display,
|
|
85
|
+
).toBe("flex");
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clickButton(label: string): HTMLButtonElement {
|
|
90
|
+
const button = requireElement(
|
|
91
|
+
document.querySelector<HTMLButtonElement>(`button[aria-label="${label}"]`),
|
|
92
|
+
`expected ${label} button`,
|
|
93
|
+
);
|
|
94
|
+
button.dispatchEvent(
|
|
95
|
+
new globalThis.MouseEvent("click", {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
cancelable: true,
|
|
98
|
+
detail: 1,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
return button;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
vi.restoreAllMocks();
|
|
106
|
+
while (editors.length > 0) editors.pop()?.destroy();
|
|
107
|
+
document.body.innerHTML = "";
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("TableControls", () => {
|
|
111
|
+
it("appears only while the focused selection is inside a table", async () => {
|
|
112
|
+
const editor = createEditor();
|
|
113
|
+
const toolbar = requireElement(
|
|
114
|
+
document.querySelector<HTMLElement>(".tiptap-table-controls"),
|
|
115
|
+
"expected table toolbar",
|
|
116
|
+
);
|
|
117
|
+
expect(toolbar.style.display).toBe("none");
|
|
118
|
+
|
|
119
|
+
await focusTable(editor);
|
|
120
|
+
expect(toolbar.style.display).toBe("flex");
|
|
121
|
+
|
|
122
|
+
editor.commands.setTextSelection(editor.state.doc.content.size - 1);
|
|
123
|
+
await vi.waitFor(() => expect(toolbar.style.display).toBe("none"));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("adds rows and columns without losing the active table selection", async () => {
|
|
127
|
+
const editor = createEditor();
|
|
128
|
+
await focusTable(editor);
|
|
129
|
+
const selectionBefore = editor.state.selection.from;
|
|
130
|
+
const addColumn = requireElement(
|
|
131
|
+
document.querySelector<HTMLButtonElement>(
|
|
132
|
+
'button[aria-label="Add column after"]',
|
|
133
|
+
),
|
|
134
|
+
"expected add-column button",
|
|
135
|
+
);
|
|
136
|
+
const pointerDown = new globalThis.MouseEvent("mousedown", {
|
|
137
|
+
bubbles: true,
|
|
138
|
+
cancelable: true,
|
|
139
|
+
});
|
|
140
|
+
addColumn.dispatchEvent(pointerDown);
|
|
141
|
+
expect(pointerDown.defaultPrevented).toBe(true);
|
|
142
|
+
expect(editor.state.selection.from).toBe(selectionBefore);
|
|
143
|
+
|
|
144
|
+
addColumn.dispatchEvent(
|
|
145
|
+
new globalThis.MouseEvent("click", { bubbles: true, detail: 1 }),
|
|
146
|
+
);
|
|
147
|
+
expect(tableDimensions(editor)).toEqual({ rows: 3, columns: 4 });
|
|
148
|
+
clickButton("Add row below");
|
|
149
|
+
expect(tableDimensions(editor)).toEqual({ rows: 4, columns: 4 });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("offers complete structural actions and protects the last row and column", async () => {
|
|
153
|
+
const editor = createEditor();
|
|
154
|
+
await focusTable(editor, 2, 2);
|
|
155
|
+
|
|
156
|
+
clickButton("Table options");
|
|
157
|
+
expect(
|
|
158
|
+
document.querySelector<HTMLElement>(".tiptap-table-options")?.hidden,
|
|
159
|
+
).toBe(false);
|
|
160
|
+
clickButton("Add row above");
|
|
161
|
+
expect(tableDimensions(editor)).toEqual({ rows: 3, columns: 2 });
|
|
162
|
+
|
|
163
|
+
clickButton("Table options");
|
|
164
|
+
clickButton("Delete row");
|
|
165
|
+
clickButton("Table options");
|
|
166
|
+
clickButton("Delete row");
|
|
167
|
+
expect(tableDimensions(editor)).toEqual({ rows: 1, columns: 2 });
|
|
168
|
+
|
|
169
|
+
editor.commands.setTextSelection(cursorPositionInRow(editor, 0));
|
|
170
|
+
clickButton("Table options");
|
|
171
|
+
expect(
|
|
172
|
+
document.querySelector<HTMLButtonElement>(
|
|
173
|
+
'button[aria-label="Delete row"]',
|
|
174
|
+
)?.disabled,
|
|
175
|
+
).toBe(true);
|
|
176
|
+
expect(
|
|
177
|
+
document.querySelector<HTMLButtonElement>(
|
|
178
|
+
'button[aria-label="Delete column"]',
|
|
179
|
+
)?.disabled,
|
|
180
|
+
).toBe(false);
|
|
181
|
+
clickButton("Delete column");
|
|
182
|
+
expect(tableDimensions(editor)).toEqual({ rows: 1, columns: 1 });
|
|
183
|
+
|
|
184
|
+
clickButton("Table options");
|
|
185
|
+
expect(
|
|
186
|
+
document.querySelector<HTMLButtonElement>(
|
|
187
|
+
'button[aria-label="Delete column"]',
|
|
188
|
+
)?.disabled,
|
|
189
|
+
).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("toggles the first header row even when the caret is in a later row", async () => {
|
|
193
|
+
const editor = createEditor();
|
|
194
|
+
await focusTable(editor, 3, 2);
|
|
195
|
+
editor.commands.setTextSelection(cursorPositionInRow(editor, 1));
|
|
196
|
+
|
|
197
|
+
clickButton("Table options");
|
|
198
|
+
clickButton("Toggle header row");
|
|
199
|
+
let rows = tableJson(editor).content ?? [];
|
|
200
|
+
expect(rows[0]?.content?.map((cell) => cell.type)).toEqual([
|
|
201
|
+
"tableCell",
|
|
202
|
+
"tableCell",
|
|
203
|
+
]);
|
|
204
|
+
expect(rows[1]?.content?.[0]?.type).toBe("tableCell");
|
|
205
|
+
|
|
206
|
+
clickButton("Table options");
|
|
207
|
+
clickButton("Toggle header row");
|
|
208
|
+
rows = tableJson(editor).content ?? [];
|
|
209
|
+
expect(rows[0]?.content?.map((cell) => cell.type)).toEqual([
|
|
210
|
+
"tableHeader",
|
|
211
|
+
"tableHeader",
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("supports keyboard entry and Escape focus restoration", async () => {
|
|
216
|
+
const editor = createEditor();
|
|
217
|
+
await focusTable(editor);
|
|
218
|
+
const shortcut = new globalThis.KeyboardEvent("keydown", {
|
|
219
|
+
key: "F10",
|
|
220
|
+
altKey: true,
|
|
221
|
+
bubbles: true,
|
|
222
|
+
cancelable: true,
|
|
223
|
+
});
|
|
224
|
+
editor.view.dom.dispatchEvent(shortcut);
|
|
225
|
+
expect(shortcut.defaultPrevented).toBe(true);
|
|
226
|
+
expect(document.activeElement?.getAttribute("aria-label")).toBe(
|
|
227
|
+
"Add row below",
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
document.activeElement?.dispatchEvent(
|
|
231
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
232
|
+
key: "Escape",
|
|
233
|
+
bubbles: true,
|
|
234
|
+
cancelable: true,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
expect(editor.view.hasFocus()).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("dismisses options outside the editor and removes floating DOM on destroy", async () => {
|
|
241
|
+
const editor = createEditor();
|
|
242
|
+
await focusTable(editor);
|
|
243
|
+
clickButton("Table options");
|
|
244
|
+
requireElement(
|
|
245
|
+
editor.view.dom.querySelector<HTMLElement>("td, th"),
|
|
246
|
+
"expected table cell",
|
|
247
|
+
).dispatchEvent(new globalThis.MouseEvent("mousedown", { bubbles: true }));
|
|
248
|
+
expect(
|
|
249
|
+
document.querySelector<HTMLElement>(".tiptap-table-options")?.hidden,
|
|
250
|
+
).toBe(true);
|
|
251
|
+
|
|
252
|
+
clickButton("Table options");
|
|
253
|
+
document.body.dispatchEvent(
|
|
254
|
+
new globalThis.MouseEvent("mousedown", { bubbles: true }),
|
|
255
|
+
);
|
|
256
|
+
expect(
|
|
257
|
+
document.querySelector<HTMLElement>(".tiptap-table-options")?.hidden,
|
|
258
|
+
).toBe(true);
|
|
259
|
+
|
|
260
|
+
editor.destroy();
|
|
261
|
+
expect(document.querySelector(".tiptap-table-controls")).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Editor, type JSONContent } from "@tiptap/core";
|
|
5
|
+
import { createEditorExtensions } from "../extensions.js";
|
|
6
|
+
import { getSlashCommands } from "../slash-commands.js";
|
|
7
|
+
import { DEFAULT_TABLE_CONTROL_LABELS } from "../table-control-labels.js";
|
|
8
|
+
|
|
9
|
+
const editors: Editor[] = [];
|
|
10
|
+
|
|
11
|
+
function requireElement<T extends globalThis.Element>(
|
|
12
|
+
element: T | null,
|
|
13
|
+
message: string,
|
|
14
|
+
): T {
|
|
15
|
+
if (!element) throw new Error(message);
|
|
16
|
+
return element;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createEditor(): Editor {
|
|
20
|
+
const element = document.createElement("div");
|
|
21
|
+
document.body.appendChild(element);
|
|
22
|
+
const editor = new Editor({
|
|
23
|
+
element,
|
|
24
|
+
extensions: createEditorExtensions({
|
|
25
|
+
toolbarMode: "compose",
|
|
26
|
+
tableControlLabels: DEFAULT_TABLE_CONTROL_LABELS,
|
|
27
|
+
}),
|
|
28
|
+
content: "<p>/table</p>",
|
|
29
|
+
});
|
|
30
|
+
vi.spyOn(editor.view, "coordsAtPos").mockImplementation(() => ({
|
|
31
|
+
left: 80,
|
|
32
|
+
right: 100,
|
|
33
|
+
top: 120,
|
|
34
|
+
bottom: 140,
|
|
35
|
+
}));
|
|
36
|
+
editor.commands.setTextSelection(7);
|
|
37
|
+
editors.push(editor);
|
|
38
|
+
return editor;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function openPicker(editor: Editor): HTMLElement {
|
|
42
|
+
const tableCommand = getSlashCommands(editor).find(
|
|
43
|
+
(item) => item.label === "Table",
|
|
44
|
+
);
|
|
45
|
+
if (!tableCommand) throw new Error("expected Table slash command");
|
|
46
|
+
tableCommand.command(editor, { from: 1, to: 7 });
|
|
47
|
+
return requireElement(
|
|
48
|
+
document.querySelector<HTMLElement>(".tiptap-table-size-picker"),
|
|
49
|
+
"expected table size picker",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tableJson(editor: Editor): JSONContent | undefined {
|
|
54
|
+
return editor.getJSON().content?.find((node) => node.type === "table");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tableDimensions(editor: Editor): { rows: number; columns: number } {
|
|
58
|
+
const rows = tableJson(editor)?.content ?? [];
|
|
59
|
+
return { rows: rows.length, columns: rows[0]?.content?.length ?? 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
while (editors.length > 0) editors.pop()?.destroy();
|
|
65
|
+
document.body.innerHTML = "";
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("table size picker", () => {
|
|
69
|
+
it("opens at 3 by 3 without inserting a table immediately", () => {
|
|
70
|
+
const editor = createEditor();
|
|
71
|
+
const picker = openPicker(editor);
|
|
72
|
+
|
|
73
|
+
expect(editor.getText()).toBe("");
|
|
74
|
+
expect(tableJson(editor)).toBeUndefined();
|
|
75
|
+
expect(picker.getAttribute("aria-label")).toContain("Insert 3 by 3 table");
|
|
76
|
+
expect(
|
|
77
|
+
picker.querySelectorAll(".tiptap-table-size-cell.is-selected"),
|
|
78
|
+
).toHaveLength(9);
|
|
79
|
+
expect(
|
|
80
|
+
picker.querySelector<HTMLButtonElement>(
|
|
81
|
+
'.tiptap-table-size-cell[data-row="3"][data-column="3"]',
|
|
82
|
+
)?.tabIndex,
|
|
83
|
+
).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("inserts the pointer-selected dimensions with a header row", () => {
|
|
87
|
+
const editor = createEditor();
|
|
88
|
+
const picker = openPicker(editor);
|
|
89
|
+
const target = requireElement(
|
|
90
|
+
picker.querySelector<HTMLButtonElement>(
|
|
91
|
+
'.tiptap-table-size-cell[data-row="4"][data-column="5"]',
|
|
92
|
+
),
|
|
93
|
+
"expected 4 by 5 option",
|
|
94
|
+
);
|
|
95
|
+
target.dispatchEvent(
|
|
96
|
+
new globalThis.PointerEvent("pointerenter", { bubbles: true }),
|
|
97
|
+
);
|
|
98
|
+
target.click();
|
|
99
|
+
|
|
100
|
+
expect(tableDimensions(editor)).toEqual({ rows: 4, columns: 5 });
|
|
101
|
+
expect(tableJson(editor)?.content?.[0]?.content?.[0]?.type).toBe(
|
|
102
|
+
"tableHeader",
|
|
103
|
+
);
|
|
104
|
+
expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("supports bounded arrow navigation and Enter insertion", async () => {
|
|
108
|
+
const editor = createEditor();
|
|
109
|
+
const picker = openPicker(editor);
|
|
110
|
+
await vi.waitFor(() => {
|
|
111
|
+
expect(document.activeElement?.getAttribute("data-row")).toBe("3");
|
|
112
|
+
});
|
|
113
|
+
const current = () =>
|
|
114
|
+
requireElement(
|
|
115
|
+
picker.querySelector<HTMLButtonElement>(
|
|
116
|
+
".tiptap-table-size-cell.is-current",
|
|
117
|
+
),
|
|
118
|
+
"expected current size",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
current().dispatchEvent(
|
|
122
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
123
|
+
key: "ArrowRight",
|
|
124
|
+
bubbles: true,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
current().dispatchEvent(
|
|
128
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
129
|
+
key: "ArrowDown",
|
|
130
|
+
bubbles: true,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
current().dispatchEvent(
|
|
134
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
135
|
+
key: "Enter",
|
|
136
|
+
bubbles: true,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(tableDimensions(editor)).toEqual({ rows: 4, columns: 4 });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("cancels on Escape and restores editor focus", async () => {
|
|
144
|
+
const editor = createEditor();
|
|
145
|
+
openPicker(editor);
|
|
146
|
+
await vi.waitFor(() => {
|
|
147
|
+
expect(document.activeElement?.getAttribute("data-row")).toBe("3");
|
|
148
|
+
});
|
|
149
|
+
document.activeElement?.dispatchEvent(
|
|
150
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
151
|
+
key: "Escape",
|
|
152
|
+
bubbles: true,
|
|
153
|
+
cancelable: true,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
|
|
158
|
+
expect(tableJson(editor)).toBeUndefined();
|
|
159
|
+
expect(editor.view.hasFocus()).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("cancels on outside click and cleans up when the editor is destroyed", async () => {
|
|
163
|
+
const editor = createEditor();
|
|
164
|
+
openPicker(editor);
|
|
165
|
+
await vi.waitFor(() => {
|
|
166
|
+
expect(document.activeElement?.getAttribute("data-row")).toBe("3");
|
|
167
|
+
});
|
|
168
|
+
document.body.dispatchEvent(
|
|
169
|
+
new globalThis.MouseEvent("mousedown", { bubbles: true }),
|
|
170
|
+
);
|
|
171
|
+
expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
|
|
172
|
+
|
|
173
|
+
editor.commands.setContent("<p>/table</p>");
|
|
174
|
+
editor.commands.setTextSelection(7);
|
|
175
|
+
openPicker(editor);
|
|
176
|
+
editor.destroy();
|
|
177
|
+
expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { Extension, type Editor } from "@tiptap/core";
|
|
10
10
|
import { Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
|
|
11
|
+
import { liftTarget } from "@tiptap/pm/transform";
|
|
11
12
|
import type { EditorView } from "@tiptap/pm/view";
|
|
12
13
|
import { isLinkToolbarInputActive } from "./link-toolbar.js";
|
|
13
14
|
import type { FormattingToolbarMode } from "./toolbar-mode.js";
|
|
@@ -70,6 +71,92 @@ export function toggleMarkAndExit(editor: Editor, markName: string): void {
|
|
|
70
71
|
.run();
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Clears presentational formatting while preserving semantic content structure.
|
|
76
|
+
*
|
|
77
|
+
* Clearable marks (bold, italic, and similar text styling) are removed while
|
|
78
|
+
* marks configured with `clearable: false`, such as links, are retained.
|
|
79
|
+
* Headings become paragraphs and selected blockquote content is lifted out of
|
|
80
|
+
* its quote wrapper. Lists, code blocks, tables, media, and other structural
|
|
81
|
+
* nodes remain intact.
|
|
82
|
+
*
|
|
83
|
+
* @param editor - Editor whose current selection should be cleared
|
|
84
|
+
* @returns Nothing
|
|
85
|
+
*/
|
|
86
|
+
export function clearFormatting(editor: Editor): void {
|
|
87
|
+
const { doc, selection } = editor.state;
|
|
88
|
+
const { from, to } = selection;
|
|
89
|
+
const headingPositions: number[] = [];
|
|
90
|
+
const blockquoteRanges: Array<{ from: number; to: number }> = [];
|
|
91
|
+
|
|
92
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
93
|
+
if (node.type === editor.schema.nodes.heading) {
|
|
94
|
+
headingPositions.push(pos);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (node.type === editor.schema.nodes.blockquote) {
|
|
98
|
+
blockquoteRanges.push({
|
|
99
|
+
from: Math.max(from, pos + 1),
|
|
100
|
+
to: Math.min(to, pos + node.nodeSize - 1),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
editor
|
|
106
|
+
.chain()
|
|
107
|
+
.focus()
|
|
108
|
+
.unsetAllMarks()
|
|
109
|
+
.command(({ tr }) => {
|
|
110
|
+
const headingType = editor.schema.nodes.heading;
|
|
111
|
+
const paragraphType = editor.schema.nodes.paragraph;
|
|
112
|
+
|
|
113
|
+
if (headingType && paragraphType) {
|
|
114
|
+
for (const position of headingPositions) {
|
|
115
|
+
const mappedPosition = tr.mapping.map(position);
|
|
116
|
+
const node = tr.doc.nodeAt(mappedPosition);
|
|
117
|
+
if (node?.type !== headingType) continue;
|
|
118
|
+
|
|
119
|
+
const $position = tr.doc.resolve(mappedPosition);
|
|
120
|
+
const index = $position.index();
|
|
121
|
+
if (
|
|
122
|
+
$position.parent.canReplaceWith(index, index + 1, paragraphType)
|
|
123
|
+
) {
|
|
124
|
+
tr.setNodeMarkup(mappedPosition, paragraphType);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const blockquoteType = editor.schema.nodes.blockquote;
|
|
130
|
+
if (blockquoteType) {
|
|
131
|
+
// Nested wrappers must be lifted from the inside out. Mapping the
|
|
132
|
+
// original ranges after each lift keeps later positions accurate.
|
|
133
|
+
for (let index = blockquoteRanges.length - 1; index >= 0; index -= 1) {
|
|
134
|
+
const range = blockquoteRanges[index];
|
|
135
|
+
if (!range) continue;
|
|
136
|
+
const mappedFrom = tr.mapping.map(range.from, 1);
|
|
137
|
+
const mappedTo = tr.mapping.map(range.to, -1);
|
|
138
|
+
if (mappedFrom > mappedTo) continue;
|
|
139
|
+
|
|
140
|
+
const nodeRange = tr.doc
|
|
141
|
+
.resolve(mappedFrom)
|
|
142
|
+
.blockRange(
|
|
143
|
+
tr.doc.resolve(mappedTo),
|
|
144
|
+
(node) => node.type === blockquoteType,
|
|
145
|
+
);
|
|
146
|
+
if (!nodeRange) continue;
|
|
147
|
+
|
|
148
|
+
const target = liftTarget(nodeRange);
|
|
149
|
+
if (target !== null) tr.lift(nodeRange, target);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const mappedEnd = tr.mapping.map(to, -1);
|
|
154
|
+
tr.setSelection(TextSelection.create(tr.doc, mappedEnd));
|
|
155
|
+
return true;
|
|
156
|
+
})
|
|
157
|
+
.run();
|
|
158
|
+
}
|
|
159
|
+
|
|
73
160
|
function getButtons(
|
|
74
161
|
editor: Editor,
|
|
75
162
|
toolbarMode: FormattingToolbarMode,
|
|
@@ -114,16 +201,7 @@ function getButtons(
|
|
|
114
201
|
key: "clear",
|
|
115
202
|
icon: ICONS.clear,
|
|
116
203
|
title: "Clear formatting",
|
|
117
|
-
action: () =>
|
|
118
|
-
const { to } = editor.state.selection;
|
|
119
|
-
editor
|
|
120
|
-
.chain()
|
|
121
|
-
.focus()
|
|
122
|
-
.unsetAllMarks()
|
|
123
|
-
.clearNodes()
|
|
124
|
-
.setTextSelection(to)
|
|
125
|
-
.run();
|
|
126
|
-
},
|
|
204
|
+
action: () => clearFormatting(editor),
|
|
127
205
|
isActive: () => false,
|
|
128
206
|
},
|
|
129
207
|
];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuous Lists
|
|
3
|
+
*
|
|
4
|
+
* Rich-text sources can paste one visual list as several adjacent list nodes.
|
|
5
|
+
* Adjacent wrappers of the same type have no visible or semantic separator, so
|
|
6
|
+
* normalize them into one list. A paragraph, a different list type, or any
|
|
7
|
+
* other block remains an intentional boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Extension } from "@tiptap/core";
|
|
11
|
+
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
12
|
+
import { Plugin } from "@tiptap/pm/state";
|
|
13
|
+
import type { EditorState, Transaction } from "@tiptap/pm/state";
|
|
14
|
+
import { canJoin } from "@tiptap/pm/transform";
|
|
15
|
+
|
|
16
|
+
function isStandardList(node: ProseMirrorNode): boolean {
|
|
17
|
+
return node.type.name === "bulletList" || node.type.name === "orderedList";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findJoinableListBoundary(
|
|
21
|
+
node: ProseMirrorNode,
|
|
22
|
+
nodePosition = -1,
|
|
23
|
+
document = node,
|
|
24
|
+
): number | null {
|
|
25
|
+
const contentStart = nodePosition + 1;
|
|
26
|
+
let childOffset = 0;
|
|
27
|
+
let previousChild: ProseMirrorNode | null = null;
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
30
|
+
const child = node.child(index);
|
|
31
|
+
const childPosition = contentStart + childOffset;
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
previousChild &&
|
|
35
|
+
isStandardList(previousChild) &&
|
|
36
|
+
previousChild.type === child.type &&
|
|
37
|
+
canJoin(document, childPosition)
|
|
38
|
+
) {
|
|
39
|
+
return childPosition;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const nestedBoundary = findJoinableListBoundary(
|
|
43
|
+
child,
|
|
44
|
+
childPosition,
|
|
45
|
+
document,
|
|
46
|
+
);
|
|
47
|
+
if (nestedBoundary !== null) return nestedBoundary;
|
|
48
|
+
|
|
49
|
+
childOffset += child.nodeSize;
|
|
50
|
+
previousChild = child;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds a transaction that joins every adjacent same-type list fragment.
|
|
58
|
+
*
|
|
59
|
+
* @param state - Current editor state
|
|
60
|
+
* @returns A joining transaction, or null when the document is normalized
|
|
61
|
+
* @example
|
|
62
|
+
* const tr = buildContinuousListsTransaction(editor.state);
|
|
63
|
+
* if (tr) editor.view.dispatch(tr);
|
|
64
|
+
*/
|
|
65
|
+
export function buildContinuousListsTransaction(
|
|
66
|
+
state: EditorState,
|
|
67
|
+
): Transaction | null {
|
|
68
|
+
const tr = state.tr;
|
|
69
|
+
let changed = false;
|
|
70
|
+
let boundary = findJoinableListBoundary(tr.doc);
|
|
71
|
+
|
|
72
|
+
while (boundary !== null) {
|
|
73
|
+
tr.join(boundary);
|
|
74
|
+
changed = true;
|
|
75
|
+
boundary = findJoinableListBoundary(tr.doc);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return changed ? tr : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const ContinuousLists = Extension.create({
|
|
82
|
+
name: "continuousLists",
|
|
83
|
+
|
|
84
|
+
onCreate() {
|
|
85
|
+
const tr = buildContinuousListsTransaction(this.editor.state);
|
|
86
|
+
if (tr) this.editor.view.dispatch(tr.setMeta("addToHistory", false));
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
addProseMirrorPlugins() {
|
|
90
|
+
return [
|
|
91
|
+
new Plugin({
|
|
92
|
+
appendTransaction: (transactions, _oldState, newState) => {
|
|
93
|
+
if (!transactions.some((transaction) => transaction.docChanged)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return buildContinuousListsTransaction(newState);
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -13,6 +13,7 @@ import type { FormattingToolbarMode } from "./toolbar-mode.js";
|
|
|
13
13
|
import type { PasteMediaOptions } from "./paste-media.js";
|
|
14
14
|
import type { RehostImagesOptions } from "./rehost-images.js";
|
|
15
15
|
import type { ImageNodeLabels } from "./image-node.js";
|
|
16
|
+
import type { TableControlLabels } from "./table-control-labels.js";
|
|
16
17
|
import { normalizeFootnoteArtifacts } from "../../lib/footnotes.js";
|
|
17
18
|
import { tiptapJsonToMarkdown } from "../../lib/tiptap-to-markdown.js";
|
|
18
19
|
import { parseMarkdownDocument } from "../../lib/markdown-manager.js";
|
|
@@ -29,6 +30,7 @@ export interface CreateEditorOptions {
|
|
|
29
30
|
pasteMedia?: PasteMediaOptions;
|
|
30
31
|
rehostImages?: RehostImagesOptions;
|
|
31
32
|
imageNodeLabels?: Partial<ImageNodeLabels>;
|
|
33
|
+
tableControlLabels?: TableControlLabels;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -46,6 +48,7 @@ export function createTiptapEditor(options: CreateEditorOptions): Editor {
|
|
|
46
48
|
pasteMedia: options.pasteMedia,
|
|
47
49
|
rehostImages: options.rehostImages,
|
|
48
50
|
imageNodeLabels: options.imageNodeLabels,
|
|
51
|
+
tableControlLabels: options.tableControlLabels,
|
|
49
52
|
}),
|
|
50
53
|
content: options.content ?? undefined,
|
|
51
54
|
editorProps: {
|