@jant/core 0.6.11 → 0.6.13
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 +47 -30
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/app-CWJFPjuR.js +6 -0
- package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/client-DCrcHVJl.css +2 -0
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
- package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
- package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +76 -2
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +10 -2
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +382 -152
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -10
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/client-auth.ts +0 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +22 -6
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +22 -6
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +77 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +85 -28
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +79 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/feed.ts +63 -3
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/navigation.ts +5 -22
- package/src/lib/post-display.ts +14 -8
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/preset.css +60 -18
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +48 -3
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +231 -1
- package/src/services/__tests__/post-timeline.test.ts +175 -2
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +7 -16
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +186 -1
- package/src/services/post.ts +103 -63
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +28 -10
- package/src/styles/ui.css +349 -13
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/SiteLayout.tsx +249 -216
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
SettingsTimezone,
|
|
22
22
|
SettingsCjkFont,
|
|
23
23
|
SettingsDashboardLanguage,
|
|
24
|
+
SettingsAboutPageStatus,
|
|
24
25
|
} from "./settings-types.js";
|
|
25
26
|
import { showToast } from "../toast.js";
|
|
26
27
|
import {
|
|
@@ -44,6 +45,12 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
44
45
|
latestFeedUrl: { type: String, attribute: "latest-feed-url" },
|
|
45
46
|
featuredFeedUrl: { type: String, attribute: "featured-feed-url" },
|
|
46
47
|
archiveFeedUrl: { type: String, attribute: "archive-feed-url" },
|
|
48
|
+
aboutPage: { type: Object, attribute: "about-page" },
|
|
49
|
+
aboutEditUrl: { type: String, attribute: "about-edit-url" },
|
|
50
|
+
aboutCreateUrl: {
|
|
51
|
+
type: String,
|
|
52
|
+
attribute: "about-create-url",
|
|
53
|
+
},
|
|
47
54
|
|
|
48
55
|
// Site group
|
|
49
56
|
_siteName: { state: true },
|
|
@@ -92,6 +99,9 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
92
99
|
declare latestFeedUrl: string;
|
|
93
100
|
declare featuredFeedUrl: string;
|
|
94
101
|
declare archiveFeedUrl: string;
|
|
102
|
+
declare aboutPage: SettingsAboutPageStatus;
|
|
103
|
+
declare aboutEditUrl: string;
|
|
104
|
+
declare aboutCreateUrl: string;
|
|
95
105
|
|
|
96
106
|
// Site
|
|
97
107
|
declare _siteName: string;
|
|
@@ -162,6 +172,12 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
162
172
|
this.latestFeedUrl = "/latest/feed";
|
|
163
173
|
this.featuredFeedUrl = "/featured/feed";
|
|
164
174
|
this.archiveFeedUrl = "/archive/feed";
|
|
175
|
+
this.aboutPage = {
|
|
176
|
+
state: "missing",
|
|
177
|
+
path: "/about",
|
|
178
|
+
};
|
|
179
|
+
this.aboutEditUrl = "/about?edit=1";
|
|
180
|
+
this.aboutCreateUrl = "/settings/general/about-page";
|
|
165
181
|
|
|
166
182
|
this._siteName = "";
|
|
167
183
|
this._siteDescription = "";
|
|
@@ -757,6 +773,45 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
757
773
|
`;
|
|
758
774
|
}
|
|
759
775
|
|
|
776
|
+
private _renderAboutPageRow() {
|
|
777
|
+
const status = this.aboutPage;
|
|
778
|
+
|
|
779
|
+
return html`
|
|
780
|
+
<div class="mt-2 text-sm text-muted-foreground" data-about-page-row>
|
|
781
|
+
${this.labels.aboutPagePrompt}
|
|
782
|
+
${status.state === "ready"
|
|
783
|
+
? html`
|
|
784
|
+
<a
|
|
785
|
+
class="font-medium text-foreground underline-offset-4 hover:underline"
|
|
786
|
+
href=${this.aboutEditUrl}
|
|
787
|
+
>
|
|
788
|
+
${this.labels.editAboutPage}
|
|
789
|
+
</a>
|
|
790
|
+
`
|
|
791
|
+
: status.state === "missing"
|
|
792
|
+
? html`
|
|
793
|
+
<form
|
|
794
|
+
class="inline"
|
|
795
|
+
method="post"
|
|
796
|
+
action=${this.aboutCreateUrl}
|
|
797
|
+
>
|
|
798
|
+
<button
|
|
799
|
+
type="submit"
|
|
800
|
+
class="inline cursor-pointer border-0 bg-transparent p-0 font-medium text-foreground underline-offset-4 hover:underline"
|
|
801
|
+
>
|
|
802
|
+
${this.labels.createAboutPage}
|
|
803
|
+
</button>
|
|
804
|
+
</form>
|
|
805
|
+
`
|
|
806
|
+
: html`
|
|
807
|
+
<span class="text-destructive"
|
|
808
|
+
>${this.labels.aboutPageConflict}</span
|
|
809
|
+
>
|
|
810
|
+
`}
|
|
811
|
+
</div>
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
|
|
760
815
|
private _renderGeneralForm() {
|
|
761
816
|
return html`
|
|
762
817
|
<div class="flex flex-col gap-8">
|
|
@@ -796,6 +851,7 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
796
851
|
<p class="text-sm text-muted-foreground mt-1">
|
|
797
852
|
${this.labels.aboutBlogHelp}
|
|
798
853
|
</p>
|
|
854
|
+
${this._renderAboutPageRow()}
|
|
799
855
|
</div>
|
|
800
856
|
|
|
801
857
|
<div class="field">
|
|
@@ -30,6 +30,16 @@ export interface NavManagerCollection {
|
|
|
30
30
|
group?: string | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface NavManagerSuggestedLink {
|
|
34
|
+
key: string;
|
|
35
|
+
label: string;
|
|
36
|
+
url: string;
|
|
37
|
+
targetType: "page" | "collection" | "archive";
|
|
38
|
+
targetLabel: string;
|
|
39
|
+
navItemType: "link" | "collection";
|
|
40
|
+
collectionId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export interface NavManagerLabels {
|
|
34
44
|
preview: string;
|
|
35
45
|
navigationItems: string;
|
|
@@ -54,6 +64,10 @@ export interface NavManagerLabels {
|
|
|
54
64
|
addLinkDescription: string;
|
|
55
65
|
urlPlaceholder: string;
|
|
56
66
|
labelAndUrlRequired: string;
|
|
67
|
+
suggestedLinks: string;
|
|
68
|
+
suggestedLinksDescription: string;
|
|
69
|
+
addSuggestedLink: string;
|
|
70
|
+
suggestedLinkAdded: string;
|
|
57
71
|
collection: string;
|
|
58
72
|
addCollection: string;
|
|
59
73
|
addCollectionToNavigation: string;
|
|
@@ -67,13 +81,3 @@ export interface NavManagerLabels {
|
|
|
67
81
|
placementSaved: string;
|
|
68
82
|
cancel: string;
|
|
69
83
|
}
|
|
70
|
-
|
|
71
|
-
export interface NavManagerUpdateDetail {
|
|
72
|
-
id: string;
|
|
73
|
-
label: string;
|
|
74
|
-
url?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface NavManagerDeleteDetail {
|
|
78
|
-
id: string;
|
|
79
|
-
}
|
|
@@ -18,6 +18,11 @@ export interface SettingsLabels {
|
|
|
18
18
|
// General
|
|
19
19
|
general: string;
|
|
20
20
|
site: string;
|
|
21
|
+
aboutPage: string;
|
|
22
|
+
aboutPagePrompt: string;
|
|
23
|
+
aboutPageConflict: string;
|
|
24
|
+
createAboutPage: string;
|
|
25
|
+
editAboutPage: string;
|
|
21
26
|
languageAndTime: string;
|
|
22
27
|
home: string;
|
|
23
28
|
search: string;
|
|
@@ -100,6 +105,31 @@ export interface SettingsInitialData {
|
|
|
100
105
|
noindex: boolean;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
export type SettingsAboutPageStatus =
|
|
109
|
+
| {
|
|
110
|
+
state: "missing";
|
|
111
|
+
path: "/about";
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
state: "ready";
|
|
115
|
+
path: "/about";
|
|
116
|
+
post: {
|
|
117
|
+
id: string;
|
|
118
|
+
title: string | null;
|
|
119
|
+
status: "draft" | "published";
|
|
120
|
+
visibility: "public" | "latest_hidden" | "private";
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
state: "conflict";
|
|
125
|
+
path: "/about";
|
|
126
|
+
conflict: {
|
|
127
|
+
targetType: "collection" | "redirect" | "archive" | "post";
|
|
128
|
+
id: string | null;
|
|
129
|
+
title: string | null;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
103
133
|
/** Event detail dispatched when a settings form is saved */
|
|
104
134
|
export interface SettingsSaveDetail {
|
|
105
135
|
endpoint: string;
|
|
@@ -57,6 +57,36 @@ export function getCurrentDetailPostArticle(
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function getLastThreadDetailArticle(
|
|
61
|
+
root: globalThis.Document | globalThis.Element,
|
|
62
|
+
): HTMLElement | null {
|
|
63
|
+
const threadGroup = root.querySelector<HTMLElement>(
|
|
64
|
+
".thread-group-detail[data-page='post']",
|
|
65
|
+
);
|
|
66
|
+
if (!threadGroup) return null;
|
|
67
|
+
|
|
68
|
+
const articles = threadGroup.querySelectorAll<HTMLElement>(
|
|
69
|
+
".thread-detail-item article[data-post]",
|
|
70
|
+
);
|
|
71
|
+
return articles.length > 0 ? articles.item(articles.length - 1) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getReplyTargetArticle(
|
|
75
|
+
root: globalThis.Document | globalThis.Element = document,
|
|
76
|
+
): HTMLElement | null {
|
|
77
|
+
const lastThreadPost = getLastThreadDetailArticle(root);
|
|
78
|
+
if (lastThreadPost) return lastThreadPost;
|
|
79
|
+
|
|
80
|
+
if (root === document) {
|
|
81
|
+
const hoveredPost = document.querySelector<HTMLElement>(
|
|
82
|
+
"[data-page='post'] article[data-post]:hover",
|
|
83
|
+
);
|
|
84
|
+
if (hoveredPost) return hoveredPost;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getCurrentDetailPostArticle(root);
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
export function getReplyRefreshTarget(
|
|
61
91
|
article: HTMLElement,
|
|
62
92
|
): ReplyRefreshTarget | null {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getActiveCollectionId,
|
|
3
3
|
getComposeDialog,
|
|
4
4
|
getCurrentDetailPostArticle,
|
|
5
|
+
getReplyTargetArticle,
|
|
5
6
|
openNewCompose,
|
|
6
7
|
openReplyForArticle,
|
|
7
8
|
} from "./compose-launch.js";
|
|
@@ -42,6 +43,34 @@ function shouldIgnoreShortcut(event: globalThis.KeyboardEvent): boolean {
|
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function hasEditQueryParam(): boolean {
|
|
47
|
+
return new URL(window.location.href).searchParams.get("edit") === "1";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function consumeEditQueryParam() {
|
|
51
|
+
const url = new URL(window.location.href);
|
|
52
|
+
url.searchParams.delete("edit");
|
|
53
|
+
const nextSearch = url.searchParams.toString();
|
|
54
|
+
globalThis.history.replaceState(
|
|
55
|
+
globalThis.history.state,
|
|
56
|
+
"",
|
|
57
|
+
`${url.pathname}${nextSearch ? `?${nextSearch}` : ""}${url.hash}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function openEditFromQueryParam() {
|
|
62
|
+
if (!document.body?.hasAttribute("data-authenticated")) return;
|
|
63
|
+
if (!hasEditQueryParam()) return;
|
|
64
|
+
|
|
65
|
+
const article = getCurrentDetailPostArticle();
|
|
66
|
+
const postId = article?.dataset.postId;
|
|
67
|
+
const composeEl = getComposeDialog();
|
|
68
|
+
if (!postId || !composeEl) return;
|
|
69
|
+
|
|
70
|
+
consumeEditQueryParam();
|
|
71
|
+
void composeEl.openEdit(postId);
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
async function toggleFeatured(
|
|
46
75
|
postId: string,
|
|
47
76
|
featured: boolean,
|
|
@@ -93,15 +122,17 @@ document.addEventListener("keydown", (event: globalThis.KeyboardEvent) => {
|
|
|
93
122
|
return;
|
|
94
123
|
}
|
|
95
124
|
|
|
96
|
-
const article = getCurrentDetailPostArticle();
|
|
97
|
-
if (!article) return;
|
|
98
|
-
|
|
99
125
|
if (key === "r") {
|
|
126
|
+
const article = getReplyTargetArticle();
|
|
127
|
+
if (!article) return;
|
|
100
128
|
event.preventDefault();
|
|
101
129
|
void openReplyForArticle(article);
|
|
102
130
|
return;
|
|
103
131
|
}
|
|
104
132
|
|
|
133
|
+
const article = getCurrentDetailPostArticle();
|
|
134
|
+
if (!article) return;
|
|
135
|
+
|
|
105
136
|
if (key === "e") {
|
|
106
137
|
const postId = article.dataset.postId;
|
|
107
138
|
if (!postId) return;
|
|
@@ -131,3 +162,15 @@ document.addEventListener("keydown", (event: globalThis.KeyboardEvent) => {
|
|
|
131
162
|
return;
|
|
132
163
|
}
|
|
133
164
|
});
|
|
165
|
+
|
|
166
|
+
if (document.readyState === "loading") {
|
|
167
|
+
document.addEventListener("DOMContentLoaded", openEditFromQueryParam, {
|
|
168
|
+
once: true,
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
openEditFromQueryParam();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const __testOnly = {
|
|
175
|
+
openEditFromQueryParam,
|
|
176
|
+
};
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Manages the "More" dropdown popover.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const moreControllers = new WeakMap();
|
|
9
|
+
const drawerControllers = new WeakMap();
|
|
10
|
+
|
|
8
11
|
function initMoreDropdown(root) {
|
|
9
12
|
const trigger = root.querySelector(".site-header-more-btn");
|
|
10
13
|
const popover = root.querySelector(".site-header-more-popover");
|
|
@@ -12,6 +15,9 @@ function initMoreDropdown(root) {
|
|
|
12
15
|
if (!trigger || !popover) return;
|
|
13
16
|
if (trigger.dataset.moreInitialized === "true") return;
|
|
14
17
|
trigger.dataset.moreInitialized = "true";
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const signal = controller.signal;
|
|
20
|
+
moreControllers.set(trigger, controller);
|
|
15
21
|
|
|
16
22
|
function open() {
|
|
17
23
|
popover.setAttribute("aria-hidden", "false");
|
|
@@ -29,30 +35,49 @@ function initMoreDropdown(root) {
|
|
|
29
35
|
if (focusTrigger) trigger.focus();
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
trigger.addEventListener(
|
|
33
|
-
|
|
34
|
-
e
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
trigger.addEventListener(
|
|
39
|
+
"click",
|
|
40
|
+
(e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
if (trigger.getAttribute("aria-expanded") === "true") {
|
|
44
|
+
close();
|
|
45
|
+
} else {
|
|
46
|
+
open();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{ signal },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
document.addEventListener(
|
|
53
|
+
"click",
|
|
54
|
+
(e) => {
|
|
55
|
+
if (!(e.target instanceof Node)) return;
|
|
56
|
+
if (!trigger.parentElement?.contains(e.target)) close();
|
|
57
|
+
},
|
|
58
|
+
{ signal },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
document.addEventListener(
|
|
62
|
+
"keydown",
|
|
63
|
+
(e) => {
|
|
64
|
+
if (
|
|
65
|
+
e.key === "Escape" &&
|
|
66
|
+
popover.getAttribute("aria-hidden") === "false"
|
|
67
|
+
) {
|
|
68
|
+
close(true);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{ signal },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
document.addEventListener(
|
|
75
|
+
"basecoat:popover",
|
|
76
|
+
(e) => {
|
|
77
|
+
if (e.detail?.source !== trigger.parentElement) close();
|
|
78
|
+
},
|
|
79
|
+
{ signal },
|
|
80
|
+
);
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
const FRESH_VISIT_KEY = "jant:nav-fresh-visits";
|
|
@@ -111,6 +136,9 @@ export function initSiteHeaderNav(root = document) {
|
|
|
111
136
|
if (!hamburger || !drawer || !backdrop) return;
|
|
112
137
|
if (hamburger.dataset.drawerInitialized === "true") return;
|
|
113
138
|
hamburger.dataset.drawerInitialized = "true";
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const signal = controller.signal;
|
|
141
|
+
drawerControllers.set(hamburger, controller);
|
|
114
142
|
|
|
115
143
|
function open() {
|
|
116
144
|
drawer.setAttribute("aria-hidden", "false");
|
|
@@ -138,35 +166,65 @@ export function initSiteHeaderNav(root = document) {
|
|
|
138
166
|
drawer.setAttribute("inert", "");
|
|
139
167
|
}
|
|
140
168
|
},
|
|
141
|
-
{ once: true },
|
|
169
|
+
{ once: true, signal },
|
|
142
170
|
);
|
|
143
171
|
|
|
144
172
|
if (returnFocus) hamburger.focus();
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
hamburger.addEventListener(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
hamburger.addEventListener(
|
|
176
|
+
"click",
|
|
177
|
+
() => {
|
|
178
|
+
if (hamburger.getAttribute("aria-expanded") === "true") {
|
|
179
|
+
close();
|
|
180
|
+
} else {
|
|
181
|
+
open();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{ signal },
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
closeBtn?.addEventListener("click", () => close(), { signal });
|
|
188
|
+
backdrop.addEventListener("click", () => close(), { signal });
|
|
189
|
+
drawer.addEventListener(
|
|
190
|
+
"click",
|
|
191
|
+
(event) => {
|
|
192
|
+
if (!(event.target instanceof Element)) return;
|
|
193
|
+
if (event.target.closest("a[href]")) {
|
|
194
|
+
close(false);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{ signal },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
drawer.addEventListener(
|
|
201
|
+
"keydown",
|
|
202
|
+
(event) => {
|
|
203
|
+
if (event.key === "Escape") {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
close();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{ signal },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
163
211
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
export function destroySiteHeaderNav(root = document) {
|
|
213
|
+
for (const trigger of root.querySelectorAll(".site-header-more-btn")) {
|
|
214
|
+
const controller = moreControllers.get(trigger);
|
|
215
|
+
controller?.abort();
|
|
216
|
+
moreControllers.delete(trigger);
|
|
217
|
+
delete trigger.dataset.moreInitialized;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const hamburger of root.querySelectorAll(".site-header-hamburger")) {
|
|
221
|
+
const controller = drawerControllers.get(hamburger);
|
|
222
|
+
controller?.abort();
|
|
223
|
+
drawerControllers.delete(hamburger);
|
|
224
|
+
delete hamburger.dataset.drawerInitialized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
document.documentElement.classList.remove("drawer-open");
|
|
170
228
|
}
|
|
171
229
|
|
|
172
230
|
initSiteHeaderNav();
|
|
@@ -62,4 +62,39 @@ describe("Block insertion flow", () => {
|
|
|
62
62
|
expect(editor.state.selection.$from.parent.type.name).toBe("paragraph");
|
|
63
63
|
expect(editor.state.selection.$from.parentOffset).toBe(0);
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it("renders a removable placeholder when an image fails to load", () => {
|
|
67
|
+
const editor = createEditor();
|
|
68
|
+
|
|
69
|
+
editor.commands.focus("start");
|
|
70
|
+
expect(editor.commands.setImage({ src: "/uploads/missing.webp" })).toBe(
|
|
71
|
+
true,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const figure = document.querySelector<HTMLElement>(".tiptap-image-figure");
|
|
75
|
+
const img = figure?.querySelector<HTMLImageElement>("img");
|
|
76
|
+
if (!figure || !img) {
|
|
77
|
+
throw new Error("expected image node view");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
img.dispatchEvent(new Event("error"));
|
|
81
|
+
|
|
82
|
+
expect(figure.dataset.loadState).toBe("missing");
|
|
83
|
+
expect(figure.textContent).toContain("Image unavailable");
|
|
84
|
+
|
|
85
|
+
const deleteButton = figure.querySelector<HTMLButtonElement>(
|
|
86
|
+
".tiptap-image-missing-delete",
|
|
87
|
+
);
|
|
88
|
+
if (!deleteButton) {
|
|
89
|
+
throw new Error("expected broken image delete button");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deleteButton.dispatchEvent(
|
|
93
|
+
new MouseEvent("mousedown", { bubbles: true, cancelable: true }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
editor.getJSON().content?.some((node) => node.type === "image"),
|
|
98
|
+
).toBe(false);
|
|
99
|
+
});
|
|
65
100
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { Editor } from "@tiptap/core";
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
6
|
+
import { ImageInputRules } from "../image-input-rules.js";
|
|
7
|
+
import { ImageNode } from "../image-node.js";
|
|
8
|
+
import { LinkInputRules } from "../link-input-rules.js";
|
|
9
|
+
|
|
10
|
+
const editors: Editor[] = [];
|
|
11
|
+
|
|
12
|
+
function createEditor(): Editor {
|
|
13
|
+
const element = document.createElement("div");
|
|
14
|
+
document.body.appendChild(element);
|
|
15
|
+
|
|
16
|
+
const editor = new Editor({
|
|
17
|
+
element,
|
|
18
|
+
extensions: [
|
|
19
|
+
StarterKit.configure({
|
|
20
|
+
heading: { levels: [1, 2, 3] },
|
|
21
|
+
link: { openOnClick: false, autolink: false },
|
|
22
|
+
}),
|
|
23
|
+
LinkInputRules,
|
|
24
|
+
ImageNode,
|
|
25
|
+
ImageInputRules,
|
|
26
|
+
],
|
|
27
|
+
content: "<p></p>",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
editors.push(editor);
|
|
31
|
+
return editor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function type(editor: Editor, text: string): void {
|
|
35
|
+
const view = editor.view;
|
|
36
|
+
for (const ch of text) {
|
|
37
|
+
const { from, to } = view.state.selection;
|
|
38
|
+
const handled = view.someProp("handleTextInput", (handler) =>
|
|
39
|
+
handler(view, from, to, ch, () => view.state.tr.insertText(ch, from, to)),
|
|
40
|
+
);
|
|
41
|
+
if (!handled) view.dispatch(view.state.tr.insertText(ch, from, to));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
while (editors.length > 0) {
|
|
47
|
+
editors.pop()?.destroy();
|
|
48
|
+
}
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("ImageInputRules", () => {
|
|
53
|
+
it("converts typed empty-alt Markdown image syntax into an image node", () => {
|
|
54
|
+
const editor = createEditor();
|
|
55
|
+
|
|
56
|
+
type(editor, "");
|
|
57
|
+
|
|
58
|
+
expect(editor.getJSON().content).toEqual([
|
|
59
|
+
{
|
|
60
|
+
type: "image",
|
|
61
|
+
attrs: {
|
|
62
|
+
src: "https://example.com/image.png",
|
|
63
|
+
alt: "",
|
|
64
|
+
title: "",
|
|
65
|
+
caption: "",
|
|
66
|
+
href: "",
|
|
67
|
+
layout: "regular",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ type: "paragraph" },
|
|
71
|
+
]);
|
|
72
|
+
expect(editor.state.selection.$from.parent.type.name).toBe("paragraph");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("preserves alt text and optional quoted title", () => {
|
|
76
|
+
const editor = createEditor();
|
|
77
|
+
|
|
78
|
+
type(editor, '');
|
|
79
|
+
|
|
80
|
+
expect(editor.getJSON().content?.[0]).toMatchObject({
|
|
81
|
+
type: "image",
|
|
82
|
+
attrs: {
|
|
83
|
+
src: "https://example.com/image.png",
|
|
84
|
+
alt: "A test image",
|
|
85
|
+
title: "Title",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("runs before Markdown link rules", () => {
|
|
91
|
+
const editor = createEditor();
|
|
92
|
+
|
|
93
|
+
type(editor, "");
|
|
94
|
+
|
|
95
|
+
expect(editor.getJSON().content?.[0]).toMatchObject({
|
|
96
|
+
type: "image",
|
|
97
|
+
attrs: {
|
|
98
|
+
src: "https://example.com/image.png",
|
|
99
|
+
alt: "Alt",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
expect(editor.state.doc.textContent).toBe("");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("leaves unsafe image URLs as typed text", () => {
|
|
106
|
+
const editor = createEditor();
|
|
107
|
+
|
|
108
|
+
type(editor, "");
|
|
109
|
+
|
|
110
|
+
expect(editor.getJSON().content).toEqual([
|
|
111
|
+
{
|
|
112
|
+
type: "paragraph",
|
|
113
|
+
content: [{ type: "text", text: "" }],
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|