@jant/core 0.3.35 → 0.3.37
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/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -0,0 +1,2161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Dialog
|
|
3
|
+
*
|
|
4
|
+
* Outer shell for the compose dialog: header with format switcher,
|
|
5
|
+
* collection selector, action row, and attachment upload coordination.
|
|
6
|
+
*
|
|
7
|
+
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LitElement, html, nothing } from "lit";
|
|
11
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
12
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
13
|
+
import type { Editor, JSONContent } from "@tiptap/core";
|
|
14
|
+
import type {
|
|
15
|
+
ComposeFormat,
|
|
16
|
+
ComposeVisibility,
|
|
17
|
+
ComposeLabels,
|
|
18
|
+
ComposeCollection,
|
|
19
|
+
ComposeSubmitDetail,
|
|
20
|
+
ComposeAttachment,
|
|
21
|
+
DraftItem,
|
|
22
|
+
LocalDraft,
|
|
23
|
+
} from "./compose-types.js";
|
|
24
|
+
import type { CollectionSubmitDetail } from "./collection-types.js";
|
|
25
|
+
import { showToast } from "../toast.js";
|
|
26
|
+
import type { JantComposeEditor } from "./jant-compose-editor.js";
|
|
27
|
+
import { getMediaCategory } from "../../lib/upload.js";
|
|
28
|
+
import { createTiptapEditor } from "../tiptap/create-editor.js";
|
|
29
|
+
import { renderCollectionIcon } from "../../lib/icons.js";
|
|
30
|
+
|
|
31
|
+
interface ReplyToData {
|
|
32
|
+
contentHtml: string;
|
|
33
|
+
dateText: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ComposeStateSnapshot {
|
|
37
|
+
format: ComposeFormat;
|
|
38
|
+
collectionIds: string[];
|
|
39
|
+
title: string;
|
|
40
|
+
bodyJson: JSONContent | null;
|
|
41
|
+
url: string;
|
|
42
|
+
quoteText: string;
|
|
43
|
+
quoteAuthor: string;
|
|
44
|
+
rating: number;
|
|
45
|
+
showTitle: boolean;
|
|
46
|
+
showRating: boolean;
|
|
47
|
+
attachments: Array<{
|
|
48
|
+
clientId: string;
|
|
49
|
+
mediaId: string | null;
|
|
50
|
+
previewUrl: string;
|
|
51
|
+
mimeType: string;
|
|
52
|
+
alt: string;
|
|
53
|
+
status: ComposeAttachment["status"];
|
|
54
|
+
summary: string | null;
|
|
55
|
+
chars: number | null;
|
|
56
|
+
}>;
|
|
57
|
+
attachedTexts: Array<{
|
|
58
|
+
clientId: string;
|
|
59
|
+
mediaId: string | null;
|
|
60
|
+
bodyJson: JSONContent | null;
|
|
61
|
+
bodyHtml: string;
|
|
62
|
+
summary: string;
|
|
63
|
+
}>;
|
|
64
|
+
attachmentOrder: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class JantComposeDialog extends LitElement {
|
|
68
|
+
static properties = {
|
|
69
|
+
collections: { type: Array },
|
|
70
|
+
labels: { type: Object },
|
|
71
|
+
uploadMaxFileSize: { type: Number, attribute: "upload-max-file-size" },
|
|
72
|
+
pageMode: { type: Boolean, attribute: "page-mode" },
|
|
73
|
+
closeHref: { type: String, attribute: "close-href" },
|
|
74
|
+
autoRestoreDraft: { type: Boolean, attribute: "auto-restore-draft" },
|
|
75
|
+
_format: { state: true },
|
|
76
|
+
_status: { state: true },
|
|
77
|
+
_loading: { state: true },
|
|
78
|
+
_collectionIds: { state: true },
|
|
79
|
+
_showCollection: { state: true },
|
|
80
|
+
_showMoreMenu: { state: true },
|
|
81
|
+
_collectionSearch: { state: true },
|
|
82
|
+
_altPanelOpen: { state: true },
|
|
83
|
+
_altPanelIndex: { state: true },
|
|
84
|
+
_attachedPanelOpen: { state: true },
|
|
85
|
+
_attachedTextIndex: { state: true },
|
|
86
|
+
_confirmPanelOpen: { state: true },
|
|
87
|
+
_editPostId: { state: true },
|
|
88
|
+
_draftSourceId: { state: true },
|
|
89
|
+
_draftsPanelOpen: { state: true },
|
|
90
|
+
_drafts: { state: true },
|
|
91
|
+
_draftsLoading: { state: true },
|
|
92
|
+
_draftsError: { state: true },
|
|
93
|
+
_draftMenuOpenId: { state: true },
|
|
94
|
+
_addCollectionPanelOpen: { state: true },
|
|
95
|
+
_replyToId: { state: true },
|
|
96
|
+
_replyToData: { state: true },
|
|
97
|
+
_replyExpanded: { state: true },
|
|
98
|
+
_visibility: { state: true },
|
|
99
|
+
_featured: { state: true },
|
|
100
|
+
_showVisibilityMenu: { state: true },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
declare collections: ComposeCollection[];
|
|
104
|
+
declare labels: ComposeLabels;
|
|
105
|
+
declare uploadMaxFileSize: number;
|
|
106
|
+
declare pageMode: boolean;
|
|
107
|
+
declare closeHref: string;
|
|
108
|
+
declare autoRestoreDraft: boolean;
|
|
109
|
+
declare _format: ComposeFormat;
|
|
110
|
+
declare _status: "published" | "draft";
|
|
111
|
+
declare _loading: boolean;
|
|
112
|
+
declare _collectionIds: string[];
|
|
113
|
+
declare _showCollection: boolean;
|
|
114
|
+
declare _showMoreMenu: boolean;
|
|
115
|
+
declare _collectionSearch: string;
|
|
116
|
+
declare _altPanelOpen: boolean;
|
|
117
|
+
declare _altPanelIndex: number;
|
|
118
|
+
declare _attachedPanelOpen: boolean;
|
|
119
|
+
declare _attachedTextIndex: number;
|
|
120
|
+
declare _confirmPanelOpen: boolean;
|
|
121
|
+
declare _editPostId: string | null;
|
|
122
|
+
declare _draftSourceId: string | null;
|
|
123
|
+
declare _draftsPanelOpen: boolean;
|
|
124
|
+
declare _drafts: DraftItem[];
|
|
125
|
+
declare _draftsLoading: boolean;
|
|
126
|
+
declare _draftsError: string | null;
|
|
127
|
+
declare _draftMenuOpenId: string | null;
|
|
128
|
+
declare _addCollectionPanelOpen: boolean;
|
|
129
|
+
declare _replyToId: string | null;
|
|
130
|
+
declare _replyToData: ReplyToData | null;
|
|
131
|
+
declare _replyExpanded: boolean;
|
|
132
|
+
declare _visibility: ComposeVisibility;
|
|
133
|
+
declare _featured: boolean;
|
|
134
|
+
declare _showVisibilityMenu: boolean;
|
|
135
|
+
|
|
136
|
+
private _attachedEditor: Editor | null = null;
|
|
137
|
+
private _attachedTextSnapshot: JSONContent | null = null;
|
|
138
|
+
private _confirmForDrafts = false;
|
|
139
|
+
private _draftSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
140
|
+
private _draftRestored = false;
|
|
141
|
+
private _initialSnapshot: string | null = null;
|
|
142
|
+
private _pageFocusApplied = false;
|
|
143
|
+
private _pageLeaveRequested = false;
|
|
144
|
+
private _suppressBeforeUnload = false;
|
|
145
|
+
|
|
146
|
+
createRenderRoot() {
|
|
147
|
+
this.innerHTML = "";
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
constructor() {
|
|
152
|
+
super();
|
|
153
|
+
this.collections = [];
|
|
154
|
+
this.labels = {} as ComposeLabels;
|
|
155
|
+
this.uploadMaxFileSize = 500;
|
|
156
|
+
this.pageMode = false;
|
|
157
|
+
this.closeHref = "/";
|
|
158
|
+
this.autoRestoreDraft = false;
|
|
159
|
+
this._format = "note";
|
|
160
|
+
this._status = "published";
|
|
161
|
+
this._loading = false;
|
|
162
|
+
this._collectionIds = [];
|
|
163
|
+
this._showCollection = false;
|
|
164
|
+
this._showMoreMenu = false;
|
|
165
|
+
this._collectionSearch = "";
|
|
166
|
+
this._altPanelOpen = false;
|
|
167
|
+
this._altPanelIndex = 0;
|
|
168
|
+
this._attachedPanelOpen = false;
|
|
169
|
+
this._attachedTextIndex = 0;
|
|
170
|
+
this._confirmPanelOpen = false;
|
|
171
|
+
this._editPostId = null;
|
|
172
|
+
this._draftSourceId = null;
|
|
173
|
+
this._draftsPanelOpen = false;
|
|
174
|
+
this._drafts = [];
|
|
175
|
+
this._draftsLoading = false;
|
|
176
|
+
this._draftsError = null;
|
|
177
|
+
this._draftMenuOpenId = null;
|
|
178
|
+
this._addCollectionPanelOpen = false;
|
|
179
|
+
this._replyToId = null;
|
|
180
|
+
this._replyToData = null;
|
|
181
|
+
this._replyExpanded = false;
|
|
182
|
+
this._visibility = "public";
|
|
183
|
+
this._featured = false;
|
|
184
|
+
this._showVisibilityMenu = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private get _editor(): JantComposeEditor | null {
|
|
188
|
+
return this.querySelector("jant-compose-editor");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected updated(changed: Map<string, unknown>) {
|
|
192
|
+
super.updated(changed);
|
|
193
|
+
if (this._initialSnapshot === null && this._editor) {
|
|
194
|
+
this._captureInitialSnapshot();
|
|
195
|
+
}
|
|
196
|
+
if (changed.has("_format") || changed.has("_collectionIds")) {
|
|
197
|
+
// Schedule draft auto-save for new-post mode only
|
|
198
|
+
if (!this._editPostId && !this._draftSourceId) {
|
|
199
|
+
this._scheduleDraftSave();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
reset() {
|
|
205
|
+
this._format = "note";
|
|
206
|
+
this._status = "published";
|
|
207
|
+
this._loading = false;
|
|
208
|
+
this._collectionIds = [];
|
|
209
|
+
this._showCollection = false;
|
|
210
|
+
this._showMoreMenu = false;
|
|
211
|
+
this._collectionSearch = "";
|
|
212
|
+
this._altPanelOpen = false;
|
|
213
|
+
this._altPanelIndex = 0;
|
|
214
|
+
this._attachedPanelOpen = false;
|
|
215
|
+
this._attachedTextIndex = 0;
|
|
216
|
+
this._confirmPanelOpen = false;
|
|
217
|
+
this._editPostId = null;
|
|
218
|
+
this._draftSourceId = null;
|
|
219
|
+
this._draftsPanelOpen = false;
|
|
220
|
+
this._drafts = [];
|
|
221
|
+
this._draftsLoading = false;
|
|
222
|
+
this._draftsError = null;
|
|
223
|
+
this._draftMenuOpenId = null;
|
|
224
|
+
this._addCollectionPanelOpen = false;
|
|
225
|
+
this._replyToId = null;
|
|
226
|
+
this._replyToData = null;
|
|
227
|
+
this._replyExpanded = false;
|
|
228
|
+
this._visibility = "public";
|
|
229
|
+
this._featured = false;
|
|
230
|
+
this._showVisibilityMenu = false;
|
|
231
|
+
this._confirmForDrafts = false;
|
|
232
|
+
this._initialSnapshot = null;
|
|
233
|
+
this._pageFocusApplied = false;
|
|
234
|
+
this._pageLeaveRequested = false;
|
|
235
|
+
this._suppressBeforeUnload = false;
|
|
236
|
+
this._destroyAttachedEditor();
|
|
237
|
+
this._editor?.reset();
|
|
238
|
+
this._captureInitialSnapshot();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async openEdit(id: string) {
|
|
242
|
+
this.reset();
|
|
243
|
+
|
|
244
|
+
const res = await fetch(`/api/posts/${id}`);
|
|
245
|
+
if (!res.ok) return;
|
|
246
|
+
const post = await res.json();
|
|
247
|
+
|
|
248
|
+
this._editPostId = id;
|
|
249
|
+
this._format = post.format;
|
|
250
|
+
|
|
251
|
+
// Pre-fill collection memberships if present
|
|
252
|
+
if (post.collectionIds?.length) {
|
|
253
|
+
this._collectionIds = post.collectionIds;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Wait for Lit to render with the new format before populating editor
|
|
257
|
+
await this.updateComplete;
|
|
258
|
+
|
|
259
|
+
// Separate text media items from other media attachments
|
|
260
|
+
const allMedia = post.mediaAttachments ?? [];
|
|
261
|
+
const nonTextMedia = allMedia.filter(
|
|
262
|
+
(m: { mimeType: string }) => !m.mimeType.startsWith("text/"),
|
|
263
|
+
);
|
|
264
|
+
const textMedia = allMedia.filter(
|
|
265
|
+
(m: { mimeType: string }) => m.mimeType === "text/x-tiptap+json",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Fetch text content for TipTap text media items (stored as { json, html } envelope)
|
|
269
|
+
const textAttachments = await Promise.all(
|
|
270
|
+
textMedia.map(
|
|
271
|
+
async (m: { id: string; url: string; summary?: string }) => {
|
|
272
|
+
try {
|
|
273
|
+
const textRes = await fetch(`/api/media/${m.id}/content`);
|
|
274
|
+
if (textRes.ok) {
|
|
275
|
+
const raw = await textRes.text();
|
|
276
|
+
const envelope = JSON.parse(raw) as {
|
|
277
|
+
json?: unknown;
|
|
278
|
+
html?: string;
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
bodyJson: JSON.stringify(envelope.json ?? {}),
|
|
282
|
+
bodyHtml: envelope.html ?? "",
|
|
283
|
+
summary: m.summary ?? "",
|
|
284
|
+
mediaId: m.id,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Fetch failed — skip
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
bodyJson: "{}",
|
|
292
|
+
bodyHtml: "",
|
|
293
|
+
summary: m.summary ?? "",
|
|
294
|
+
mediaId: m.id,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
this._editor?.populate({
|
|
301
|
+
format: post.format,
|
|
302
|
+
title: post.title ?? undefined,
|
|
303
|
+
bodyJson: post.body ?? undefined,
|
|
304
|
+
url: post.url ?? undefined,
|
|
305
|
+
quoteText: post.quoteText ?? undefined,
|
|
306
|
+
quoteAuthor:
|
|
307
|
+
post.format === "quote" ? (post.title ?? undefined) : undefined,
|
|
308
|
+
rating: post.rating ?? undefined,
|
|
309
|
+
media: nonTextMedia.map(
|
|
310
|
+
(m: {
|
|
311
|
+
id: string;
|
|
312
|
+
previewUrl: string;
|
|
313
|
+
alt?: string;
|
|
314
|
+
mimeType: string;
|
|
315
|
+
}) => ({
|
|
316
|
+
id: m.id,
|
|
317
|
+
previewUrl: m.previewUrl,
|
|
318
|
+
alt: m.alt,
|
|
319
|
+
mimeType: m.mimeType,
|
|
320
|
+
}),
|
|
321
|
+
),
|
|
322
|
+
textAttachments,
|
|
323
|
+
attachmentOrder: allMedia.map((m: { id: string }) => m.id),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.closest("dialog")?.showModal();
|
|
327
|
+
globalThis.requestAnimationFrame(() => {
|
|
328
|
+
this._editor?.focusInput();
|
|
329
|
+
this._captureInitialSnapshot();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Open compose dialog in reply mode.
|
|
335
|
+
*
|
|
336
|
+
* @param id - UUID of the post being replied to
|
|
337
|
+
* @param replyData - Pre-captured content from the DOM (avoids API fetch)
|
|
338
|
+
*/
|
|
339
|
+
async openReply(id: string, replyData?: ReplyToData) {
|
|
340
|
+
this.reset();
|
|
341
|
+
this._replyToId = id;
|
|
342
|
+
this._replyToData = replyData ?? null;
|
|
343
|
+
this._format = "note";
|
|
344
|
+
|
|
345
|
+
this.closest("dialog")?.showModal();
|
|
346
|
+
await this.updateComplete;
|
|
347
|
+
this._editor?.focusInput();
|
|
348
|
+
this._captureInitialSnapshot();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fetch parent post from API to populate reply context preview.
|
|
353
|
+
* Falls back gracefully if the parent is unavailable (deleted, etc.).
|
|
354
|
+
*/
|
|
355
|
+
private async _fetchReplyContext(replyToId: string) {
|
|
356
|
+
try {
|
|
357
|
+
const res = await fetch(`/api/posts/${replyToId}`);
|
|
358
|
+
if (!res.ok) return;
|
|
359
|
+
const post = await res.json();
|
|
360
|
+
const dateText = post.publishedAt
|
|
361
|
+
? new Date(post.publishedAt * 1000).toLocaleDateString(undefined, {
|
|
362
|
+
month: "short",
|
|
363
|
+
day: "numeric",
|
|
364
|
+
})
|
|
365
|
+
: "";
|
|
366
|
+
this._replyToData = {
|
|
367
|
+
contentHtml: (post.bodyHtml as string) ?? "",
|
|
368
|
+
dateText,
|
|
369
|
+
};
|
|
370
|
+
} catch {
|
|
371
|
+
// Parent unavailable — reply mode still works, just no preview
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
set loading(v: boolean) {
|
|
376
|
+
this._loading = v;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private _closeDialog() {
|
|
380
|
+
const dialog = this.closest("dialog");
|
|
381
|
+
if (dialog) {
|
|
382
|
+
dialog.close();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.pageMode) {
|
|
387
|
+
this._suppressBeforeUnload = true;
|
|
388
|
+
globalThis.location.assign(this.closeHref || "/");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
requestCloseAndLeave() {
|
|
393
|
+
this._pageLeaveRequested = true;
|
|
394
|
+
this.requestClose();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
consumePageLeaveRequest(): boolean {
|
|
398
|
+
const shouldLeave = this._pageLeaveRequested;
|
|
399
|
+
this._pageLeaveRequested = false;
|
|
400
|
+
return shouldLeave;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
preparePageLeave() {
|
|
404
|
+
this._suppressBeforeUnload = true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private _hasContent(): boolean {
|
|
408
|
+
const editor = this._editor;
|
|
409
|
+
if (!editor) return false;
|
|
410
|
+
|
|
411
|
+
const data = editor.getData();
|
|
412
|
+
if (data.body) return true;
|
|
413
|
+
if (data.title.trim()) return true;
|
|
414
|
+
if (data.url.trim()) return true;
|
|
415
|
+
if (data.quoteText.trim()) return true;
|
|
416
|
+
if (data.quoteAuthor.trim()) return true;
|
|
417
|
+
if (data.attachedTexts.some((t) => t.bodyJson !== null)) return true;
|
|
418
|
+
if (data.rating > 0) return true;
|
|
419
|
+
if (data.attachments.length > 0) return true;
|
|
420
|
+
// Collection selection alone isn't content — it's metadata that
|
|
421
|
+
// only matters when paired with actual post content above.
|
|
422
|
+
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private _buildSnapshot(): ComposeStateSnapshot | null {
|
|
427
|
+
const editor = this._editor;
|
|
428
|
+
if (!editor) return null;
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
format: this._format,
|
|
432
|
+
collectionIds: [...this._collectionIds],
|
|
433
|
+
title: editor._title,
|
|
434
|
+
bodyJson: editor._bodyJson,
|
|
435
|
+
url: editor._url,
|
|
436
|
+
quoteText: editor._quoteText,
|
|
437
|
+
quoteAuthor: editor._quoteAuthor,
|
|
438
|
+
rating: editor._rating,
|
|
439
|
+
showTitle: editor._showTitle,
|
|
440
|
+
showRating: editor._showRating,
|
|
441
|
+
attachments: editor._attachments.map((attachment) => ({
|
|
442
|
+
clientId: attachment.clientId,
|
|
443
|
+
mediaId: attachment.mediaId,
|
|
444
|
+
previewUrl: attachment.previewUrl,
|
|
445
|
+
mimeType: attachment.file.type,
|
|
446
|
+
alt: attachment.alt,
|
|
447
|
+
status: attachment.status,
|
|
448
|
+
summary: attachment.summary,
|
|
449
|
+
chars: attachment.chars,
|
|
450
|
+
})),
|
|
451
|
+
attachedTexts: editor._attachedTexts.map((item) => ({
|
|
452
|
+
clientId: item.clientId,
|
|
453
|
+
mediaId: item.mediaId ?? null,
|
|
454
|
+
bodyJson: item.bodyJson,
|
|
455
|
+
bodyHtml: item.bodyHtml,
|
|
456
|
+
summary: item.summary,
|
|
457
|
+
})),
|
|
458
|
+
attachmentOrder: [...editor._attachmentOrder],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private _serializeSnapshot(
|
|
463
|
+
snapshot: ComposeStateSnapshot | null,
|
|
464
|
+
): string | null {
|
|
465
|
+
if (!snapshot) return null;
|
|
466
|
+
return JSON.stringify(snapshot);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private _captureInitialSnapshot() {
|
|
470
|
+
this._initialSnapshot = this._serializeSnapshot(this._buildSnapshot());
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private _hasUnsavedChanges(): boolean {
|
|
474
|
+
const currentSnapshot = this._serializeSnapshot(this._buildSnapshot());
|
|
475
|
+
if (currentSnapshot === null) return false;
|
|
476
|
+
if (this._initialSnapshot === null) return this._hasContent();
|
|
477
|
+
return currentSnapshot !== this._initialSnapshot;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
requestClose() {
|
|
481
|
+
if (this._loading) return;
|
|
482
|
+
|
|
483
|
+
// Dismiss any open dropdowns first
|
|
484
|
+
if (this._showCollection) {
|
|
485
|
+
this._showCollection = false;
|
|
486
|
+
this._collectionSearch = "";
|
|
487
|
+
}
|
|
488
|
+
if (this._showMoreMenu) {
|
|
489
|
+
this._showMoreMenu = false;
|
|
490
|
+
}
|
|
491
|
+
if (this._showVisibilityMenu) {
|
|
492
|
+
this._showVisibilityMenu = false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (this._confirmPanelOpen) {
|
|
496
|
+
this._confirmPanelOpen = false;
|
|
497
|
+
this._confirmForDrafts = false;
|
|
498
|
+
this._pageLeaveRequested = false;
|
|
499
|
+
this.updateComplete.then(() => this._editor?.focusInput());
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// In edit mode, only prompt if actual changes were made
|
|
504
|
+
if (this._editPostId) {
|
|
505
|
+
if (this._hasUnsavedChanges()) {
|
|
506
|
+
this._confirmForDrafts = false;
|
|
507
|
+
this._confirmPanelOpen = true;
|
|
508
|
+
} else {
|
|
509
|
+
this._closeDialog();
|
|
510
|
+
this.reset();
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (this._hasContent()) {
|
|
516
|
+
this._confirmForDrafts = false;
|
|
517
|
+
this._confirmPanelOpen = true;
|
|
518
|
+
} else {
|
|
519
|
+
this._closeDialog();
|
|
520
|
+
this.reset();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private _discardAndClose() {
|
|
525
|
+
if (this._draftSourceId) {
|
|
526
|
+
const id = this._draftSourceId;
|
|
527
|
+
fetch(`/api/posts/${id}`, { method: "DELETE" }).catch(() => {});
|
|
528
|
+
showToast(this.labels.draftDeleted);
|
|
529
|
+
}
|
|
530
|
+
this._clearDraftFromStorage();
|
|
531
|
+
this._confirmPanelOpen = false;
|
|
532
|
+
this._closeDialog();
|
|
533
|
+
(document.activeElement as HTMLElement)?.blur();
|
|
534
|
+
this.reset();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private _handleConfirmSave() {
|
|
538
|
+
if (this._confirmForDrafts) {
|
|
539
|
+
this._dispatchSubmit("draft");
|
|
540
|
+
this._confirmPanelOpen = false;
|
|
541
|
+
this.reset();
|
|
542
|
+
this._openDraftsPanel();
|
|
543
|
+
} else if (this._editPostId) {
|
|
544
|
+
// Editing a published post — publish the update directly
|
|
545
|
+
this._confirmPanelOpen = false;
|
|
546
|
+
this._submit("published");
|
|
547
|
+
} else {
|
|
548
|
+
this._confirmPanelOpen = false;
|
|
549
|
+
this._submit("draft");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private _handleConfirmDiscard() {
|
|
554
|
+
if (this._confirmForDrafts) {
|
|
555
|
+
if (this._draftSourceId) {
|
|
556
|
+
const id = this._draftSourceId;
|
|
557
|
+
fetch(`/api/posts/${id}`, { method: "DELETE" }).catch(() => {});
|
|
558
|
+
showToast(this.labels.draftDeleted);
|
|
559
|
+
}
|
|
560
|
+
this._confirmPanelOpen = false;
|
|
561
|
+
this.reset();
|
|
562
|
+
this._openDraftsPanel();
|
|
563
|
+
} else {
|
|
564
|
+
this._discardAndClose();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private _buildSubmitDetail(
|
|
569
|
+
status: "published" | "draft",
|
|
570
|
+
): ComposeSubmitDetail | null {
|
|
571
|
+
const editor = this._editor;
|
|
572
|
+
if (!editor) return null;
|
|
573
|
+
|
|
574
|
+
const editorData = editor.getData();
|
|
575
|
+
const attachments = editorData.attachments ?? [];
|
|
576
|
+
|
|
577
|
+
// Collect mediaIds from completed uploads
|
|
578
|
+
const mediaIds = attachments
|
|
579
|
+
.filter((a) => a.status === "done" && a.mediaId)
|
|
580
|
+
.map((a) => a.mediaId as string);
|
|
581
|
+
|
|
582
|
+
// Collect alt text keyed by mediaId
|
|
583
|
+
const mediaAlts: Record<string, string> = {};
|
|
584
|
+
for (const a of attachments) {
|
|
585
|
+
if (a.mediaId && a.alt) {
|
|
586
|
+
mediaAlts[a.mediaId] = a.alt;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Capture clientId → mediaId for all done attachments now,
|
|
591
|
+
// because the editor will be reset before the deferred handler runs
|
|
592
|
+
const mediaClientMap: Record<string, string> = {};
|
|
593
|
+
for (const a of attachments) {
|
|
594
|
+
if (a.mediaId) {
|
|
595
|
+
mediaClientMap[a.clientId] = a.mediaId;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
format: this._format,
|
|
601
|
+
title: editorData.title,
|
|
602
|
+
body: editorData.body,
|
|
603
|
+
url: editorData.url,
|
|
604
|
+
quoteText: editorData.quoteText,
|
|
605
|
+
quoteAuthor: editorData.quoteAuthor,
|
|
606
|
+
status,
|
|
607
|
+
visibility: this._visibility,
|
|
608
|
+
featured: this._featured || undefined,
|
|
609
|
+
rating: editorData.rating,
|
|
610
|
+
collectionIds: [...this._collectionIds],
|
|
611
|
+
mediaIds,
|
|
612
|
+
mediaAlts,
|
|
613
|
+
attachedTexts: editorData.attachedTexts,
|
|
614
|
+
attachmentOrder: editorData.attachmentOrder ?? [],
|
|
615
|
+
mediaClientMap,
|
|
616
|
+
editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
|
|
617
|
+
replyToId: this._replyToId ?? undefined,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private _dispatchSubmit(status: "published" | "draft"): boolean {
|
|
622
|
+
if (this._loading) return false;
|
|
623
|
+
const editor = this._editor;
|
|
624
|
+
if (!editor) return false;
|
|
625
|
+
|
|
626
|
+
const detail = this._buildSubmitDetail(status);
|
|
627
|
+
if (!detail) return false;
|
|
628
|
+
|
|
629
|
+
const attachments = editor._attachments ?? [];
|
|
630
|
+
const pendingAttachments = attachments.filter(
|
|
631
|
+
(a) =>
|
|
632
|
+
a.status === "pending" ||
|
|
633
|
+
a.status === "processing" ||
|
|
634
|
+
a.status === "uploading",
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
this.dispatchEvent(
|
|
638
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
639
|
+
bubbles: true,
|
|
640
|
+
detail: { ...detail, pendingAttachments },
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private _submit(status: "published" | "draft") {
|
|
647
|
+
this._clearDraftFromStorage();
|
|
648
|
+
if (!this._dispatchSubmit(status)) return;
|
|
649
|
+
if (this.pageMode) {
|
|
650
|
+
this._loading = true;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this._closeDialog();
|
|
654
|
+
// Prevent browser from restoring focus to the trigger button
|
|
655
|
+
(document.activeElement as HTMLElement)?.blur();
|
|
656
|
+
this.reset();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private _toggleCollection(id: string) {
|
|
660
|
+
if (this._collectionIds.includes(id)) {
|
|
661
|
+
this._collectionIds = this._collectionIds.filter((cid) => cid !== id);
|
|
662
|
+
} else {
|
|
663
|
+
this._collectionIds = [...this._collectionIds, id];
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private _selectedCollectionLabel(collections: ComposeCollection[]): string {
|
|
668
|
+
const ids = this._collectionIds;
|
|
669
|
+
const first = collections.find((c) => c.id === ids[0]);
|
|
670
|
+
if (!first) return "";
|
|
671
|
+
if (ids.length === 1) return first.title;
|
|
672
|
+
return this.labels.collectionCountLabel
|
|
673
|
+
.replace("%name%", first.title)
|
|
674
|
+
.replace("%count%", String(ids.length - 1));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
connectedCallback() {
|
|
678
|
+
super.connectedCallback();
|
|
679
|
+
this.addEventListener("keydown", this._handleKeydown);
|
|
680
|
+
this.addEventListener("jant:alt-panel-open", this._handleAltPanelOpen);
|
|
681
|
+
this.addEventListener("jant:alt-panel-close", this._handleAltPanelClose);
|
|
682
|
+
this.addEventListener(
|
|
683
|
+
"jant:attached-panel-open",
|
|
684
|
+
this._handleAttachedPanelOpen,
|
|
685
|
+
);
|
|
686
|
+
this.addEventListener(
|
|
687
|
+
"jant:compose-content-changed",
|
|
688
|
+
this._onContentChanged,
|
|
689
|
+
);
|
|
690
|
+
// Listen on document — fullscreen element lives on document.body, outside the dialog
|
|
691
|
+
document.addEventListener(
|
|
692
|
+
"jant:fullscreen-close",
|
|
693
|
+
this._handleFullscreenClose as EventListener,
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// Flush pending draft save before page unload (covers refresh/close mid-debounce)
|
|
697
|
+
window.addEventListener("beforeunload", this._onBeforeUnload);
|
|
698
|
+
|
|
699
|
+
// Intercept native dialog cancel (ESC) to route through requestClose
|
|
700
|
+
const dialog = this.closest("dialog");
|
|
701
|
+
if (dialog) {
|
|
702
|
+
dialog.addEventListener("cancel", this._handleDialogCancel);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (this.pageMode) {
|
|
706
|
+
this.updateComplete.then(() => this._focusPageEditorOnMount());
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
disconnectedCallback() {
|
|
711
|
+
super.disconnectedCallback();
|
|
712
|
+
this.removeEventListener("keydown", this._handleKeydown);
|
|
713
|
+
this.removeEventListener("jant:alt-panel-open", this._handleAltPanelOpen);
|
|
714
|
+
this.removeEventListener("jant:alt-panel-close", this._handleAltPanelClose);
|
|
715
|
+
this.removeEventListener(
|
|
716
|
+
"jant:attached-panel-open",
|
|
717
|
+
this._handleAttachedPanelOpen,
|
|
718
|
+
);
|
|
719
|
+
this.removeEventListener(
|
|
720
|
+
"jant:compose-content-changed",
|
|
721
|
+
this._onContentChanged,
|
|
722
|
+
);
|
|
723
|
+
document.removeEventListener(
|
|
724
|
+
"jant:fullscreen-close",
|
|
725
|
+
this._handleFullscreenClose as EventListener,
|
|
726
|
+
);
|
|
727
|
+
window.removeEventListener("beforeunload", this._onBeforeUnload);
|
|
728
|
+
this._destroyAttachedEditor();
|
|
729
|
+
this._cancelDraftSaveTimer();
|
|
730
|
+
|
|
731
|
+
const dialog = this.closest("dialog");
|
|
732
|
+
if (dialog) {
|
|
733
|
+
dialog.removeEventListener("cancel", this._handleDialogCancel);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private _handleDialogCancel = (e: Event) => {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
this.requestClose();
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
private _handleKeydown = (e: Event) => {
|
|
743
|
+
const ke = e as globalThis.KeyboardEvent;
|
|
744
|
+
if (ke.key === "Escape") {
|
|
745
|
+
ke.preventDefault();
|
|
746
|
+
ke.stopPropagation();
|
|
747
|
+
if (this._showCollection) {
|
|
748
|
+
this._showCollection = false;
|
|
749
|
+
this._collectionSearch = "";
|
|
750
|
+
} else if (this._showMoreMenu) {
|
|
751
|
+
this._showMoreMenu = false;
|
|
752
|
+
} else if (this._showVisibilityMenu) {
|
|
753
|
+
this._showVisibilityMenu = false;
|
|
754
|
+
} else if (this._addCollectionPanelOpen) {
|
|
755
|
+
this._addCollectionPanelOpen = false;
|
|
756
|
+
} else if (this._draftMenuOpenId) {
|
|
757
|
+
this._draftMenuOpenId = null;
|
|
758
|
+
} else if (this._draftsPanelOpen) {
|
|
759
|
+
this._closeDraftsPanel();
|
|
760
|
+
} else if (this._attachedPanelOpen) {
|
|
761
|
+
this._cancelAttachedPanel();
|
|
762
|
+
} else {
|
|
763
|
+
this.requestClose();
|
|
764
|
+
}
|
|
765
|
+
} else if (ke.key === "Enter" && this._confirmPanelOpen) {
|
|
766
|
+
ke.preventDefault();
|
|
767
|
+
this._handleConfirmSave();
|
|
768
|
+
} else if ((ke.metaKey || ke.ctrlKey) && ke.key === "Enter") {
|
|
769
|
+
e.preventDefault();
|
|
770
|
+
this._submit("published");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
private _handleAltPanelOpen = (e: Event) => {
|
|
775
|
+
const detail = (e as CustomEvent<{ index: number }>).detail;
|
|
776
|
+
this._altPanelIndex = detail.index;
|
|
777
|
+
this._altPanelOpen = true;
|
|
778
|
+
this.updateComplete.then(() => {
|
|
779
|
+
this.querySelector<HTMLInputElement>(".compose-alt-input")?.focus();
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
private _handleAltPanelClose = () => {
|
|
784
|
+
this._altPanelOpen = false;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
private _getAltAttachment(): ComposeAttachment | null {
|
|
788
|
+
return this._editor?._attachments[this._altPanelIndex] ?? null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private _onAltInput(e: Event) {
|
|
792
|
+
const value = (e.target as HTMLInputElement).value;
|
|
793
|
+
this._editor?.updateAlt(this._altPanelIndex, value);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private _closeAltPanel() {
|
|
797
|
+
this._altPanelOpen = false;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private _handleFullscreenClose = (
|
|
801
|
+
e: CustomEvent<{ json: unknown; title: string }>,
|
|
802
|
+
) => {
|
|
803
|
+
const editor = this._editor;
|
|
804
|
+
if (editor) {
|
|
805
|
+
editor.setEditorState(
|
|
806
|
+
e.detail.json as import("@tiptap/core").JSONContent,
|
|
807
|
+
e.detail.title,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
private _handleAttachedPanelOpen = (e: Event) => {
|
|
813
|
+
const detail = (e as CustomEvent<{ index: number }>).detail;
|
|
814
|
+
this._attachedTextIndex = detail.index;
|
|
815
|
+
this._attachedPanelOpen = true;
|
|
816
|
+
this.updateComplete.then(() => {
|
|
817
|
+
const container = this.querySelector<HTMLElement>(
|
|
818
|
+
".compose-attached-tiptap",
|
|
819
|
+
);
|
|
820
|
+
if (!container) return;
|
|
821
|
+
const item = this._editor?._attachedTexts[this._attachedTextIndex];
|
|
822
|
+
const content = item?.bodyJson ?? null;
|
|
823
|
+
this._attachedTextSnapshot = content
|
|
824
|
+
? JSON.parse(JSON.stringify(content))
|
|
825
|
+
: null;
|
|
826
|
+
this._attachedEditor = createTiptapEditor({
|
|
827
|
+
element: container,
|
|
828
|
+
placeholder: this.labels.attachedTextPlaceholder,
|
|
829
|
+
content,
|
|
830
|
+
});
|
|
831
|
+
this._attachedEditor.commands.focus();
|
|
832
|
+
});
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
private _isAttachedTextDirty(): boolean {
|
|
836
|
+
if (!this._attachedEditor) return false;
|
|
837
|
+
return (
|
|
838
|
+
JSON.stringify(this._attachedEditor.getJSON()) !==
|
|
839
|
+
JSON.stringify(this._attachedTextSnapshot)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private _destroyAttachedEditor() {
|
|
844
|
+
if (this._attachedEditor) {
|
|
845
|
+
this._attachedEditor.destroy();
|
|
846
|
+
this._attachedEditor = null;
|
|
847
|
+
}
|
|
848
|
+
this._attachedTextSnapshot = null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private _doneAttachedPanel() {
|
|
852
|
+
if (this._attachedEditor) {
|
|
853
|
+
const json = this._attachedEditor.getJSON();
|
|
854
|
+
const html = this._attachedEditor.getHTML();
|
|
855
|
+
this._editor?.updateAttachedText(this._attachedTextIndex, json, html);
|
|
856
|
+
}
|
|
857
|
+
this._destroyAttachedEditor();
|
|
858
|
+
this._attachedPanelOpen = false;
|
|
859
|
+
this._editor?.closeAttachedPanel(this._attachedTextIndex);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private _cancelAttachedPanel() {
|
|
863
|
+
if (this._isAttachedTextDirty()) {
|
|
864
|
+
if (!globalThis.confirm("Discard changes?")) return;
|
|
865
|
+
}
|
|
866
|
+
// Revert to snapshot — don't save current editor content
|
|
867
|
+
this._destroyAttachedEditor();
|
|
868
|
+
this._attachedPanelOpen = false;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ── Drafts panel ─────────────────────────────────────────────────
|
|
872
|
+
|
|
873
|
+
private _handleDraftButtonClick() {
|
|
874
|
+
if (this._loading) return;
|
|
875
|
+
if (this._hasContent()) {
|
|
876
|
+
this._confirmForDrafts = true;
|
|
877
|
+
this._confirmPanelOpen = true;
|
|
878
|
+
} else {
|
|
879
|
+
this._openDraftsPanel();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private async _openDraftsPanel() {
|
|
884
|
+
this._draftsPanelOpen = true;
|
|
885
|
+
this._draftsLoading = true;
|
|
886
|
+
this._draftsError = null;
|
|
887
|
+
this._draftMenuOpenId = null;
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
const res = await fetch("/api/posts?status=draft&limit=50");
|
|
891
|
+
if (!res.ok) throw new Error("Failed to load drafts");
|
|
892
|
+
const json = await res.json();
|
|
893
|
+
const posts = json.posts ?? json;
|
|
894
|
+
this._drafts = (posts as Record<string, unknown>[]).map(
|
|
895
|
+
(p): DraftItem => ({
|
|
896
|
+
id: p.id as string,
|
|
897
|
+
format: p.format as ComposeFormat,
|
|
898
|
+
title: (p.title as string) ?? null,
|
|
899
|
+
bodyText: (p.bodyText as string) ?? null,
|
|
900
|
+
bodyHtml: (p.bodyHtml as string) ?? null,
|
|
901
|
+
url: (p.url as string) ?? null,
|
|
902
|
+
quoteText: (p.quoteText as string) ?? null,
|
|
903
|
+
replyToId: (p.replyToId as string) ?? null,
|
|
904
|
+
updatedAt: p.updatedAt as number,
|
|
905
|
+
mediaAttachments: (
|
|
906
|
+
(p.mediaAttachments as DraftItem["mediaAttachments"]) ?? []
|
|
907
|
+
).map((m) => ({
|
|
908
|
+
id: m.id,
|
|
909
|
+
previewUrl: m.previewUrl,
|
|
910
|
+
alt: m.alt,
|
|
911
|
+
mimeType: m.mimeType,
|
|
912
|
+
})),
|
|
913
|
+
}),
|
|
914
|
+
);
|
|
915
|
+
} catch {
|
|
916
|
+
this._draftsError = "Could not load drafts. Try again.";
|
|
917
|
+
this._drafts = [];
|
|
918
|
+
} finally {
|
|
919
|
+
this._draftsLoading = false;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private _closeDraftsPanel() {
|
|
924
|
+
this._draftsPanelOpen = false;
|
|
925
|
+
this._draftMenuOpenId = null;
|
|
926
|
+
this.updateComplete.then(() => this._editor?.focusInput());
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private async _loadDraft(id: string) {
|
|
930
|
+
this._draftsPanelOpen = false;
|
|
931
|
+
this._draftMenuOpenId = null;
|
|
932
|
+
this.reset();
|
|
933
|
+
|
|
934
|
+
const res = await fetch(`/api/posts/${id}`);
|
|
935
|
+
if (!res.ok) return;
|
|
936
|
+
const post = await res.json();
|
|
937
|
+
|
|
938
|
+
this._draftSourceId = id;
|
|
939
|
+
this._format = post.format;
|
|
940
|
+
|
|
941
|
+
if (post.collectionIds?.length) {
|
|
942
|
+
this._collectionIds = post.collectionIds;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Restore reply context if this draft was a reply
|
|
946
|
+
if (post.replyToId) {
|
|
947
|
+
this._replyToId = post.replyToId;
|
|
948
|
+
await this._fetchReplyContext(post.replyToId);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
await this.updateComplete;
|
|
952
|
+
|
|
953
|
+
// Separate text media items from other media attachments
|
|
954
|
+
const allMedia = post.mediaAttachments ?? [];
|
|
955
|
+
const nonTextMedia = allMedia.filter(
|
|
956
|
+
(m: { mimeType: string }) => !m.mimeType.startsWith("text/"),
|
|
957
|
+
);
|
|
958
|
+
const textMedia = allMedia.filter(
|
|
959
|
+
(m: { mimeType: string }) => m.mimeType === "text/x-tiptap+json",
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
// Fetch text content for TipTap text media items (stored as { json, html } envelope)
|
|
963
|
+
const textAttachments = await Promise.all(
|
|
964
|
+
textMedia.map(
|
|
965
|
+
async (m: { id: string; url: string; summary?: string }) => {
|
|
966
|
+
try {
|
|
967
|
+
const textRes = await fetch(`/api/media/${m.id}/content`);
|
|
968
|
+
if (textRes.ok) {
|
|
969
|
+
const raw = await textRes.text();
|
|
970
|
+
const envelope = JSON.parse(raw) as {
|
|
971
|
+
json?: unknown;
|
|
972
|
+
html?: string;
|
|
973
|
+
};
|
|
974
|
+
return {
|
|
975
|
+
bodyJson: JSON.stringify(envelope.json ?? {}),
|
|
976
|
+
bodyHtml: envelope.html ?? "",
|
|
977
|
+
summary: m.summary ?? "",
|
|
978
|
+
mediaId: m.id,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
} catch {
|
|
982
|
+
// Fetch failed — skip
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
bodyJson: "{}",
|
|
986
|
+
bodyHtml: "",
|
|
987
|
+
summary: m.summary ?? "",
|
|
988
|
+
mediaId: m.id,
|
|
989
|
+
};
|
|
990
|
+
},
|
|
991
|
+
),
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
this._editor?.populate({
|
|
995
|
+
format: post.format,
|
|
996
|
+
title: post.title ?? undefined,
|
|
997
|
+
bodyJson: post.body ?? undefined,
|
|
998
|
+
url: post.url ?? undefined,
|
|
999
|
+
quoteText: post.quoteText ?? undefined,
|
|
1000
|
+
quoteAuthor:
|
|
1001
|
+
post.format === "quote" ? (post.title ?? undefined) : undefined,
|
|
1002
|
+
rating: post.rating ?? undefined,
|
|
1003
|
+
media: nonTextMedia.map(
|
|
1004
|
+
(m: {
|
|
1005
|
+
id: string;
|
|
1006
|
+
previewUrl: string;
|
|
1007
|
+
alt?: string;
|
|
1008
|
+
mimeType: string;
|
|
1009
|
+
}) => ({
|
|
1010
|
+
id: m.id,
|
|
1011
|
+
previewUrl: m.previewUrl,
|
|
1012
|
+
alt: m.alt,
|
|
1013
|
+
mimeType: m.mimeType,
|
|
1014
|
+
}),
|
|
1015
|
+
),
|
|
1016
|
+
textAttachments,
|
|
1017
|
+
attachmentOrder: allMedia.map((m: { id: string }) => m.id),
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
globalThis.requestAnimationFrame(() => {
|
|
1021
|
+
this._editor?.focusInput();
|
|
1022
|
+
this._captureInitialSnapshot();
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private async _deleteDraft(id: string) {
|
|
1027
|
+
this._draftMenuOpenId = null;
|
|
1028
|
+
this._drafts = this._drafts.filter((d) => d.id !== id);
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const res = await fetch(`/api/posts/${id}`, { method: "DELETE" });
|
|
1032
|
+
if (!res.ok) throw new Error();
|
|
1033
|
+
showToast(this.labels.draftDeleted);
|
|
1034
|
+
} catch {
|
|
1035
|
+
showToast("Failed to delete draft. Try again.", "error");
|
|
1036
|
+
this._openDraftsPanel();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private _formatDraftDate(timestamp: number): string {
|
|
1041
|
+
const now = Date.now() / 1000;
|
|
1042
|
+
const diff = now - timestamp;
|
|
1043
|
+
if (diff < 60) return "now";
|
|
1044
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
|
1045
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
|
1046
|
+
if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
|
|
1047
|
+
const d = new Date(timestamp * 1000);
|
|
1048
|
+
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private _getDraftPreview(draft: DraftItem): string | null {
|
|
1052
|
+
if (draft.bodyText) return draft.bodyText;
|
|
1053
|
+
if (draft.title) return draft.title;
|
|
1054
|
+
if (draft.quoteText) return draft.quoteText;
|
|
1055
|
+
if (draft.url) return draft.url;
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ── Local draft auto-save (globalThis.localStorage) ──────────────────────────
|
|
1060
|
+
|
|
1061
|
+
private static _DRAFT_KEY = "jant:compose-draft";
|
|
1062
|
+
private static _DRAFT_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
1063
|
+
|
|
1064
|
+
private _onContentChanged = () => {
|
|
1065
|
+
// Schedule localStorage auto-save for new-post mode only
|
|
1066
|
+
if (!this._editPostId && !this._draftSourceId) {
|
|
1067
|
+
this._scheduleDraftSave();
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
private _cancelDraftSaveTimer() {
|
|
1072
|
+
if (this._draftSaveTimer !== null) {
|
|
1073
|
+
clearTimeout(this._draftSaveTimer);
|
|
1074
|
+
this._draftSaveTimer = null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private _scheduleDraftSave() {
|
|
1079
|
+
this._cancelDraftSaveTimer();
|
|
1080
|
+
this._draftSaveTimer = setTimeout(() => this._saveDraftToStorage(), 1000);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** Flush pending draft save and warn on unsaved changes before page unload */
|
|
1084
|
+
private _onBeforeUnload = (e: globalThis.BeforeUnloadEvent) => {
|
|
1085
|
+
if (this._suppressBeforeUnload) return;
|
|
1086
|
+
|
|
1087
|
+
// Flush any pending debounced draft save
|
|
1088
|
+
if (this._draftSaveTimer !== null) {
|
|
1089
|
+
this._cancelDraftSaveTimer();
|
|
1090
|
+
this._saveDraftToStorage();
|
|
1091
|
+
}
|
|
1092
|
+
// Warn if compose has unsaved modifications in either dialog or page mode.
|
|
1093
|
+
const dialog = this.closest("dialog");
|
|
1094
|
+
const shouldWarn =
|
|
1095
|
+
this._hasUnsavedChanges() && (this.pageMode || dialog?.open === true);
|
|
1096
|
+
if (shouldWarn) {
|
|
1097
|
+
e.preventDefault();
|
|
1098
|
+
e.returnValue = "";
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
private _saveDraftToStorage() {
|
|
1103
|
+
const editor = this._editor;
|
|
1104
|
+
if (!editor) return;
|
|
1105
|
+
|
|
1106
|
+
const data = editor.getData();
|
|
1107
|
+
const hasContent =
|
|
1108
|
+
!!data.body ||
|
|
1109
|
+
!!data.title.trim() ||
|
|
1110
|
+
!!data.url.trim() ||
|
|
1111
|
+
!!data.quoteText.trim() ||
|
|
1112
|
+
!!data.quoteAuthor.trim() ||
|
|
1113
|
+
data.rating > 0 ||
|
|
1114
|
+
data.attachedTexts.some((t) => t.bodyJson !== null);
|
|
1115
|
+
|
|
1116
|
+
if (!hasContent) {
|
|
1117
|
+
globalThis.localStorage.removeItem(JantComposeDialog._DRAFT_KEY);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const draft: LocalDraft = {
|
|
1122
|
+
format: this._format,
|
|
1123
|
+
title: data.title,
|
|
1124
|
+
bodyJson: editor._bodyJson,
|
|
1125
|
+
url: data.url,
|
|
1126
|
+
quoteText: data.quoteText,
|
|
1127
|
+
quoteAuthor: data.quoteAuthor,
|
|
1128
|
+
rating: data.rating,
|
|
1129
|
+
showTitle: editor._showTitle,
|
|
1130
|
+
showRating: editor._showRating,
|
|
1131
|
+
collectionIds: [...this._collectionIds],
|
|
1132
|
+
replyToId: this._replyToId,
|
|
1133
|
+
attachedTexts: data.attachedTexts.map((t) => ({
|
|
1134
|
+
clientId: t.clientId,
|
|
1135
|
+
bodyJson: t.bodyJson,
|
|
1136
|
+
bodyHtml: t.bodyHtml,
|
|
1137
|
+
summary: t.summary,
|
|
1138
|
+
})),
|
|
1139
|
+
attachmentOrder: [...(data.attachmentOrder ?? [])],
|
|
1140
|
+
savedAt: Date.now(),
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
globalThis.localStorage.setItem(
|
|
1145
|
+
JantComposeDialog._DRAFT_KEY,
|
|
1146
|
+
JSON.stringify(draft),
|
|
1147
|
+
);
|
|
1148
|
+
} catch {
|
|
1149
|
+
// Storage full or unavailable — silently ignore
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private _clearDraftFromStorage() {
|
|
1154
|
+
this._cancelDraftSaveTimer();
|
|
1155
|
+
globalThis.localStorage.removeItem(JantComposeDialog._DRAFT_KEY);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async restoreLocalDraft() {
|
|
1159
|
+
// Don't restore if already in edit or draft-load mode
|
|
1160
|
+
if (this._editPostId || this._draftSourceId) return;
|
|
1161
|
+
// Don't restore if the editor already has content (e.g. reopened dialog)
|
|
1162
|
+
if (this._hasContent()) return;
|
|
1163
|
+
|
|
1164
|
+
let raw: string | null;
|
|
1165
|
+
try {
|
|
1166
|
+
raw = globalThis.localStorage.getItem(JantComposeDialog._DRAFT_KEY);
|
|
1167
|
+
} catch {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (!raw) return;
|
|
1171
|
+
|
|
1172
|
+
let draft: LocalDraft;
|
|
1173
|
+
try {
|
|
1174
|
+
draft = JSON.parse(raw) as LocalDraft;
|
|
1175
|
+
} catch {
|
|
1176
|
+
globalThis.localStorage.removeItem(JantComposeDialog._DRAFT_KEY);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Discard stale drafts
|
|
1181
|
+
if (Date.now() - draft.savedAt > JantComposeDialog._DRAFT_MAX_AGE) {
|
|
1182
|
+
globalThis.localStorage.removeItem(JantComposeDialog._DRAFT_KEY);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
this._format = draft.format;
|
|
1187
|
+
this._collectionIds = [...(draft.collectionIds ?? [])];
|
|
1188
|
+
|
|
1189
|
+
// Restore reply context if this draft was a reply
|
|
1190
|
+
if (draft.replyToId) {
|
|
1191
|
+
this._replyToId = draft.replyToId;
|
|
1192
|
+
await this._fetchReplyContext(draft.replyToId);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
await this.updateComplete;
|
|
1196
|
+
|
|
1197
|
+
const textAttachments = draft.attachedTexts
|
|
1198
|
+
?.filter((t) => t.bodyJson !== null)
|
|
1199
|
+
.map((t) => ({
|
|
1200
|
+
clientId: t.clientId,
|
|
1201
|
+
bodyJson: JSON.stringify(t.bodyJson),
|
|
1202
|
+
bodyHtml: t.bodyHtml,
|
|
1203
|
+
summary: t.summary,
|
|
1204
|
+
}));
|
|
1205
|
+
|
|
1206
|
+
this._editor?.populate({
|
|
1207
|
+
format: draft.format,
|
|
1208
|
+
title: draft.title || undefined,
|
|
1209
|
+
bodyJson: draft.bodyJson ? JSON.stringify(draft.bodyJson) : undefined,
|
|
1210
|
+
url: draft.url || undefined,
|
|
1211
|
+
quoteText: draft.quoteText || undefined,
|
|
1212
|
+
quoteAuthor: draft.quoteAuthor || undefined,
|
|
1213
|
+
rating: draft.rating || undefined,
|
|
1214
|
+
showTitle: draft.showTitle,
|
|
1215
|
+
showRating: draft.showRating,
|
|
1216
|
+
textAttachments: textAttachments?.length ? textAttachments : undefined,
|
|
1217
|
+
attachmentOrder: draft.attachmentOrder,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
this._draftRestored = true;
|
|
1221
|
+
showToast(this.labels.draftRestored);
|
|
1222
|
+
globalThis.requestAnimationFrame(() => {
|
|
1223
|
+
this._captureInitialSnapshot();
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private async _focusPageEditorOnMount() {
|
|
1228
|
+
if (this._pageFocusApplied) return;
|
|
1229
|
+
|
|
1230
|
+
if (this.autoRestoreDraft) {
|
|
1231
|
+
await this.restoreLocalDraft();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
await this.updateComplete;
|
|
1235
|
+
globalThis.requestAnimationFrame(() => {
|
|
1236
|
+
this._editor?.focusInput();
|
|
1237
|
+
this._pageFocusApplied = true;
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private _renderDraftsPanel() {
|
|
1242
|
+
if (!this._draftsPanelOpen) return nothing;
|
|
1243
|
+
|
|
1244
|
+
return html`
|
|
1245
|
+
<div class="compose-drafts-panel">
|
|
1246
|
+
<div class="compose-alt-header">
|
|
1247
|
+
<button
|
|
1248
|
+
type="button"
|
|
1249
|
+
class="compose-attached-panel-back"
|
|
1250
|
+
@click=${() => this._closeDraftsPanel()}
|
|
1251
|
+
>
|
|
1252
|
+
<svg
|
|
1253
|
+
class="icon-fine"
|
|
1254
|
+
width="16"
|
|
1255
|
+
height="16"
|
|
1256
|
+
viewBox="0 0 16 16"
|
|
1257
|
+
fill="none"
|
|
1258
|
+
stroke="currentColor"
|
|
1259
|
+
stroke-width="1.5"
|
|
1260
|
+
stroke-linecap="round"
|
|
1261
|
+
stroke-linejoin="round"
|
|
1262
|
+
>
|
|
1263
|
+
<path d="M11 3L6 8l5 5" />
|
|
1264
|
+
</svg>
|
|
1265
|
+
</button>
|
|
1266
|
+
<span class="compose-alt-title">${this.labels.drafts}</span>
|
|
1267
|
+
</div>
|
|
1268
|
+
${this._draftsLoading
|
|
1269
|
+
? html`<div class="compose-drafts-loading">
|
|
1270
|
+
<svg
|
|
1271
|
+
class="animate-spin size-5"
|
|
1272
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1273
|
+
viewBox="0 0 24 24"
|
|
1274
|
+
fill="none"
|
|
1275
|
+
stroke="currentColor"
|
|
1276
|
+
stroke-width="2"
|
|
1277
|
+
stroke-linecap="round"
|
|
1278
|
+
stroke-linejoin="round"
|
|
1279
|
+
>
|
|
1280
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
1281
|
+
</svg>
|
|
1282
|
+
</div>`
|
|
1283
|
+
: this._draftsError
|
|
1284
|
+
? html`<div class="compose-drafts-empty">${this._draftsError}</div>`
|
|
1285
|
+
: this._drafts.length === 0
|
|
1286
|
+
? html`<div class="compose-drafts-empty">
|
|
1287
|
+
${this.labels.draftsEmpty}
|
|
1288
|
+
</div>`
|
|
1289
|
+
: html`<div class="compose-drafts-list">
|
|
1290
|
+
${this._drafts.map(
|
|
1291
|
+
(draft, i) => html`
|
|
1292
|
+
${i > 0
|
|
1293
|
+
? html`<div class="compose-drafts-divider"></div>`
|
|
1294
|
+
: nothing}
|
|
1295
|
+
${this._renderDraftItem(draft)}
|
|
1296
|
+
`,
|
|
1297
|
+
)}
|
|
1298
|
+
</div>`}
|
|
1299
|
+
</div>
|
|
1300
|
+
`;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
private _renderDraftItem(draft: DraftItem) {
|
|
1304
|
+
const preview = this._getDraftPreview(draft);
|
|
1305
|
+
|
|
1306
|
+
return html`
|
|
1307
|
+
<div class="compose-draft-item" @click=${() => this._loadDraft(draft.id)}>
|
|
1308
|
+
<div class="compose-draft-content">
|
|
1309
|
+
${preview
|
|
1310
|
+
? html`<div class="compose-draft-preview">${preview}</div>`
|
|
1311
|
+
: html`<div
|
|
1312
|
+
class="compose-draft-preview compose-draft-preview-empty"
|
|
1313
|
+
>
|
|
1314
|
+
Empty draft
|
|
1315
|
+
</div>`}
|
|
1316
|
+
<div class="compose-draft-meta">
|
|
1317
|
+
${this._formatDraftDate(draft.updatedAt)}
|
|
1318
|
+
</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
<div class="relative">
|
|
1321
|
+
${this._draftMenuOpenId === draft.id
|
|
1322
|
+
? html`<div
|
|
1323
|
+
class="compose-dropdown-backdrop"
|
|
1324
|
+
@click=${(e: Event) => {
|
|
1325
|
+
e.stopPropagation();
|
|
1326
|
+
this._draftMenuOpenId = null;
|
|
1327
|
+
}}
|
|
1328
|
+
></div>`
|
|
1329
|
+
: nothing}
|
|
1330
|
+
<button
|
|
1331
|
+
type="button"
|
|
1332
|
+
class="compose-draft-more"
|
|
1333
|
+
@click=${(e: Event) => {
|
|
1334
|
+
e.stopPropagation();
|
|
1335
|
+
this._draftMenuOpenId =
|
|
1336
|
+
this._draftMenuOpenId === draft.id ? null : draft.id;
|
|
1337
|
+
}}
|
|
1338
|
+
>
|
|
1339
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1340
|
+
<circle cx="4" cy="8" r="1.2" />
|
|
1341
|
+
<circle cx="8" cy="8" r="1.2" />
|
|
1342
|
+
<circle cx="12" cy="8" r="1.2" />
|
|
1343
|
+
</svg>
|
|
1344
|
+
</button>
|
|
1345
|
+
${this._draftMenuOpenId === draft.id
|
|
1346
|
+
? html`
|
|
1347
|
+
<div class="compose-dropdown compose-dropdown-right">
|
|
1348
|
+
<button
|
|
1349
|
+
type="button"
|
|
1350
|
+
class="compose-dropdown-item compose-dropdown-item-danger"
|
|
1351
|
+
@click=${(e: Event) => {
|
|
1352
|
+
e.stopPropagation();
|
|
1353
|
+
this._deleteDraft(draft.id);
|
|
1354
|
+
}}
|
|
1355
|
+
>
|
|
1356
|
+
${this.labels.deleteDraft}
|
|
1357
|
+
</button>
|
|
1358
|
+
</div>
|
|
1359
|
+
`
|
|
1360
|
+
: nothing}
|
|
1361
|
+
</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
`;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// ── Reply context rendering ──────────────────────────────────────
|
|
1367
|
+
|
|
1368
|
+
private _renderReplyContext() {
|
|
1369
|
+
if (!this._replyToId || !this._replyToData) return nothing;
|
|
1370
|
+
|
|
1371
|
+
const { contentHtml, dateText } = this._replyToData;
|
|
1372
|
+
const isExpanded = this._replyExpanded;
|
|
1373
|
+
|
|
1374
|
+
return html`
|
|
1375
|
+
<div class="compose-reply-row">
|
|
1376
|
+
<div class="compose-thread-dot"></div>
|
|
1377
|
+
<div
|
|
1378
|
+
class=${classMap({
|
|
1379
|
+
"compose-reply-context": true,
|
|
1380
|
+
expanded: isExpanded,
|
|
1381
|
+
})}
|
|
1382
|
+
>
|
|
1383
|
+
<div class="compose-reply-context-body">
|
|
1384
|
+
${unsafeHTML(contentHtml)}
|
|
1385
|
+
</div>
|
|
1386
|
+
${!isExpanded
|
|
1387
|
+
? html`<div class="compose-reply-fade"></div>`
|
|
1388
|
+
: nothing}
|
|
1389
|
+
</div>
|
|
1390
|
+
</div>
|
|
1391
|
+
<div class="compose-reply-meta">
|
|
1392
|
+
${dateText ? html`<span>${dateText}</span><span>·</span>` : nothing}
|
|
1393
|
+
<button
|
|
1394
|
+
type="button"
|
|
1395
|
+
class="compose-reply-toggle"
|
|
1396
|
+
@click=${() => {
|
|
1397
|
+
this._replyExpanded = !this._replyExpanded;
|
|
1398
|
+
}}
|
|
1399
|
+
>
|
|
1400
|
+
${isExpanded ? this.labels.showLess : this.labels.showMore}
|
|
1401
|
+
</button>
|
|
1402
|
+
</div>
|
|
1403
|
+
`;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// ── Render helpers ────────────────────────────────────────────────
|
|
1407
|
+
|
|
1408
|
+
private _renderHeader() {
|
|
1409
|
+
const formats: ComposeFormat[] = ["note", "link", "quote"];
|
|
1410
|
+
const formatLabels: Record<ComposeFormat, string> = {
|
|
1411
|
+
note: this.labels.note,
|
|
1412
|
+
link: this.labels.link,
|
|
1413
|
+
quote: this.labels.quote,
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
return html`
|
|
1417
|
+
<header class="compose-dialog-header">
|
|
1418
|
+
<button
|
|
1419
|
+
type="button"
|
|
1420
|
+
class="compose-dialog-cancel"
|
|
1421
|
+
@click=${() => this.requestClose()}
|
|
1422
|
+
>
|
|
1423
|
+
${this.labels.cancel}
|
|
1424
|
+
</button>
|
|
1425
|
+
|
|
1426
|
+
<div class="compose-dialog-header-center">
|
|
1427
|
+
${this._editPostId
|
|
1428
|
+
? html`<span class="compose-dialog-title"
|
|
1429
|
+
>${this.labels.editPost}</span
|
|
1430
|
+
>`
|
|
1431
|
+
: html`
|
|
1432
|
+
<div class="compose-segmented">
|
|
1433
|
+
<div
|
|
1434
|
+
class=${classMap({
|
|
1435
|
+
"compose-format-pill": true,
|
|
1436
|
+
"compose-format-pill-link": this._format === "link",
|
|
1437
|
+
"compose-format-pill-quote": this._format === "quote",
|
|
1438
|
+
})}
|
|
1439
|
+
></div>
|
|
1440
|
+
${formats.map(
|
|
1441
|
+
(f) => html`
|
|
1442
|
+
<button
|
|
1443
|
+
type="button"
|
|
1444
|
+
class=${classMap({
|
|
1445
|
+
"compose-segmented-item": true,
|
|
1446
|
+
"compose-segmented-item-active": this._format === f,
|
|
1447
|
+
})}
|
|
1448
|
+
@click=${() => {
|
|
1449
|
+
this._format = f;
|
|
1450
|
+
globalThis.requestAnimationFrame(() =>
|
|
1451
|
+
this._editor?.focusInput(),
|
|
1452
|
+
);
|
|
1453
|
+
}}
|
|
1454
|
+
>
|
|
1455
|
+
${formatLabels[f]}
|
|
1456
|
+
</button>
|
|
1457
|
+
`,
|
|
1458
|
+
)}
|
|
1459
|
+
</div>
|
|
1460
|
+
`}
|
|
1461
|
+
</div>
|
|
1462
|
+
|
|
1463
|
+
<div class="flex items-center gap-0.5 shrink-0">
|
|
1464
|
+
${this._editPostId
|
|
1465
|
+
? nothing
|
|
1466
|
+
: html`<button
|
|
1467
|
+
type="button"
|
|
1468
|
+
class="compose-dialog-header-btn"
|
|
1469
|
+
title=${this.labels.saveDraft}
|
|
1470
|
+
?disabled=${this._loading}
|
|
1471
|
+
@click=${() => this._handleDraftButtonClick()}
|
|
1472
|
+
>
|
|
1473
|
+
<svg
|
|
1474
|
+
class="icon-fine"
|
|
1475
|
+
width="18"
|
|
1476
|
+
height="18"
|
|
1477
|
+
viewBox="0 0 18 18"
|
|
1478
|
+
fill="none"
|
|
1479
|
+
stroke="currentColor"
|
|
1480
|
+
stroke-width="1.3"
|
|
1481
|
+
stroke-linecap="round"
|
|
1482
|
+
stroke-linejoin="round"
|
|
1483
|
+
>
|
|
1484
|
+
<path d="M14 2.5L15.5 4 7 12.5l-3 .5.5-3L14 2.5z" />
|
|
1485
|
+
<path d="M4 15h10" />
|
|
1486
|
+
</svg>
|
|
1487
|
+
</button>`}
|
|
1488
|
+
${this._renderMoreMenu()}
|
|
1489
|
+
</div>
|
|
1490
|
+
</header>
|
|
1491
|
+
`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private _renderMoreMenu() {
|
|
1495
|
+
return html`
|
|
1496
|
+
<div class="relative">
|
|
1497
|
+
${this._showMoreMenu
|
|
1498
|
+
? html`<div
|
|
1499
|
+
class="compose-dropdown-backdrop"
|
|
1500
|
+
@click=${() => {
|
|
1501
|
+
this._showMoreMenu = false;
|
|
1502
|
+
}}
|
|
1503
|
+
></div>`
|
|
1504
|
+
: nothing}
|
|
1505
|
+
<button
|
|
1506
|
+
type="button"
|
|
1507
|
+
class="compose-dialog-header-btn"
|
|
1508
|
+
@click=${() => {
|
|
1509
|
+
this._showMoreMenu = !this._showMoreMenu;
|
|
1510
|
+
}}
|
|
1511
|
+
>
|
|
1512
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
|
|
1513
|
+
<circle cx="4.5" cy="9" r="1.3" />
|
|
1514
|
+
<circle cx="9" cy="9" r="1.3" />
|
|
1515
|
+
<circle cx="13.5" cy="9" r="1.3" />
|
|
1516
|
+
</svg>
|
|
1517
|
+
</button>
|
|
1518
|
+
${this._showMoreMenu
|
|
1519
|
+
? html`
|
|
1520
|
+
<div class="compose-dropdown compose-dropdown-right">
|
|
1521
|
+
<button
|
|
1522
|
+
type="button"
|
|
1523
|
+
class="compose-dropdown-item"
|
|
1524
|
+
@click=${() => {
|
|
1525
|
+
this._submit("draft");
|
|
1526
|
+
this._showMoreMenu = false;
|
|
1527
|
+
}}
|
|
1528
|
+
>
|
|
1529
|
+
${this.labels.saveAsDraft}
|
|
1530
|
+
</button>
|
|
1531
|
+
<div class="compose-dropdown-divider"></div>
|
|
1532
|
+
<button
|
|
1533
|
+
type="button"
|
|
1534
|
+
class="compose-dropdown-item compose-dropdown-item-danger"
|
|
1535
|
+
@click=${() => {
|
|
1536
|
+
this._showMoreMenu = false;
|
|
1537
|
+
this._discardAndClose();
|
|
1538
|
+
}}
|
|
1539
|
+
>
|
|
1540
|
+
${this.labels.discard}
|
|
1541
|
+
</button>
|
|
1542
|
+
</div>
|
|
1543
|
+
`
|
|
1544
|
+
: nothing}
|
|
1545
|
+
</div>
|
|
1546
|
+
`;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
private _renderCollectionSelector() {
|
|
1550
|
+
const collections = this.collections ?? [];
|
|
1551
|
+
const search = this._collectionSearch.toLowerCase();
|
|
1552
|
+
const filtered = search
|
|
1553
|
+
? collections.filter((c) => c.title.toLowerCase().includes(search))
|
|
1554
|
+
: collections;
|
|
1555
|
+
const selectedCount = this._collectionIds.length;
|
|
1556
|
+
|
|
1557
|
+
return html`
|
|
1558
|
+
<div class="flex-1 min-w-0">
|
|
1559
|
+
${this._showCollection
|
|
1560
|
+
? html`<div
|
|
1561
|
+
class="compose-dropdown-backdrop"
|
|
1562
|
+
@click=${() => {
|
|
1563
|
+
this._showCollection = false;
|
|
1564
|
+
this._collectionSearch = "";
|
|
1565
|
+
}}
|
|
1566
|
+
></div>`
|
|
1567
|
+
: nothing}
|
|
1568
|
+
<div class="select compose-collection-select" data-select-initialized>
|
|
1569
|
+
<button
|
|
1570
|
+
type="button"
|
|
1571
|
+
class="compose-collection-trigger"
|
|
1572
|
+
@click=${() => {
|
|
1573
|
+
this._showCollection = !this._showCollection;
|
|
1574
|
+
if (!this._showCollection) {
|
|
1575
|
+
this._collectionSearch = "";
|
|
1576
|
+
}
|
|
1577
|
+
}}
|
|
1578
|
+
>
|
|
1579
|
+
<svg
|
|
1580
|
+
width="14"
|
|
1581
|
+
height="14"
|
|
1582
|
+
viewBox="0 0 18 18"
|
|
1583
|
+
fill="none"
|
|
1584
|
+
stroke="currentColor"
|
|
1585
|
+
stroke-width="1.4"
|
|
1586
|
+
stroke-linecap="round"
|
|
1587
|
+
stroke-linejoin="round"
|
|
1588
|
+
class="shrink-0 icon-fine"
|
|
1589
|
+
>
|
|
1590
|
+
<rect x="3" y="5" width="12" height="10" rx="2" />
|
|
1591
|
+
<path d="M6 5V4a1 1 0 011-1h4a1 1 0 011 1v1" />
|
|
1592
|
+
</svg>
|
|
1593
|
+
${selectedCount > 0
|
|
1594
|
+
? html`<span class="compose-collection-label"
|
|
1595
|
+
>${this._selectedCollectionLabel(collections)}</span
|
|
1596
|
+
>`
|
|
1597
|
+
: html`<span>${this.labels.collection}</span>`}
|
|
1598
|
+
<svg
|
|
1599
|
+
width="10"
|
|
1600
|
+
height="10"
|
|
1601
|
+
viewBox="0 0 10 10"
|
|
1602
|
+
fill="none"
|
|
1603
|
+
stroke="currentColor"
|
|
1604
|
+
stroke-width="1.4"
|
|
1605
|
+
stroke-linecap="round"
|
|
1606
|
+
stroke-linejoin="round"
|
|
1607
|
+
class="shrink-0 opacity-50 icon-fine"
|
|
1608
|
+
>
|
|
1609
|
+
<path d="M3 4l2 2 2-2" />
|
|
1610
|
+
</svg>
|
|
1611
|
+
</button>
|
|
1612
|
+
<div
|
|
1613
|
+
data-popover
|
|
1614
|
+
data-side="top"
|
|
1615
|
+
aria-hidden=${this._showCollection ? "false" : "true"}
|
|
1616
|
+
>
|
|
1617
|
+
${collections.length > 0
|
|
1618
|
+
? html`<header>
|
|
1619
|
+
<svg
|
|
1620
|
+
width="16"
|
|
1621
|
+
height="16"
|
|
1622
|
+
viewBox="0 0 24 24"
|
|
1623
|
+
fill="none"
|
|
1624
|
+
stroke="currentColor"
|
|
1625
|
+
stroke-width="2"
|
|
1626
|
+
stroke-linecap="round"
|
|
1627
|
+
stroke-linejoin="round"
|
|
1628
|
+
>
|
|
1629
|
+
<circle cx="11" cy="11" r="8" />
|
|
1630
|
+
<path d="m21 21-4.3-4.3" />
|
|
1631
|
+
</svg>
|
|
1632
|
+
<input
|
|
1633
|
+
type="text"
|
|
1634
|
+
role="combobox"
|
|
1635
|
+
placeholder=${this.labels.searchCollections}
|
|
1636
|
+
autocomplete="off"
|
|
1637
|
+
autocorrect="off"
|
|
1638
|
+
spellcheck="false"
|
|
1639
|
+
.value=${this._collectionSearch}
|
|
1640
|
+
@input=${(e: Event) => {
|
|
1641
|
+
this._collectionSearch = (
|
|
1642
|
+
e.target as HTMLInputElement
|
|
1643
|
+
).value;
|
|
1644
|
+
}}
|
|
1645
|
+
/>
|
|
1646
|
+
</header>`
|
|
1647
|
+
: nothing}
|
|
1648
|
+
<div
|
|
1649
|
+
role="listbox"
|
|
1650
|
+
aria-multiselectable="true"
|
|
1651
|
+
data-empty=${filtered.length === 0
|
|
1652
|
+
? search
|
|
1653
|
+
? this.labels.noCollections
|
|
1654
|
+
: this.labels.emptyCollections
|
|
1655
|
+
: nothing}
|
|
1656
|
+
>
|
|
1657
|
+
${filtered.map(
|
|
1658
|
+
(col) => html`
|
|
1659
|
+
<div
|
|
1660
|
+
role="option"
|
|
1661
|
+
data-value=${col.id}
|
|
1662
|
+
aria-selected=${this._collectionIds.includes(col.id)
|
|
1663
|
+
? "true"
|
|
1664
|
+
: nothing}
|
|
1665
|
+
@click=${() => this._toggleCollection(col.id)}
|
|
1666
|
+
>
|
|
1667
|
+
${col.iconHtml
|
|
1668
|
+
? html`<span
|
|
1669
|
+
class="inline-flex items-center justify-center w-4 h-4 shrink-0"
|
|
1670
|
+
>${unsafeHTML(col.iconHtml)}</span
|
|
1671
|
+
>`
|
|
1672
|
+
: nothing}
|
|
1673
|
+
${col.title}
|
|
1674
|
+
</div>
|
|
1675
|
+
`,
|
|
1676
|
+
)}
|
|
1677
|
+
</div>
|
|
1678
|
+
<div
|
|
1679
|
+
class="compose-collection-add-action"
|
|
1680
|
+
@click=${() => {
|
|
1681
|
+
this._showCollection = false;
|
|
1682
|
+
this._collectionSearch = "";
|
|
1683
|
+
this._addCollectionPanelOpen = true;
|
|
1684
|
+
}}
|
|
1685
|
+
>
|
|
1686
|
+
<svg
|
|
1687
|
+
width="14"
|
|
1688
|
+
height="14"
|
|
1689
|
+
viewBox="0 0 16 16"
|
|
1690
|
+
fill="none"
|
|
1691
|
+
stroke="currentColor"
|
|
1692
|
+
stroke-width="1.5"
|
|
1693
|
+
stroke-linecap="round"
|
|
1694
|
+
stroke-linejoin="round"
|
|
1695
|
+
>
|
|
1696
|
+
<path d="M8 3v10M3 8h10" />
|
|
1697
|
+
</svg>
|
|
1698
|
+
${this.labels.addCollection}
|
|
1699
|
+
</div>
|
|
1700
|
+
</div>
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
`;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// ── Add Collection panel ────────────────────────────────────────
|
|
1707
|
+
|
|
1708
|
+
private async _handleAddCollectionSubmit(e: Event) {
|
|
1709
|
+
const event = e as CustomEvent<CollectionSubmitDetail>;
|
|
1710
|
+
event.stopPropagation();
|
|
1711
|
+
|
|
1712
|
+
const detail = event.detail;
|
|
1713
|
+
if (!detail) return;
|
|
1714
|
+
|
|
1715
|
+
const formEl = this.querySelector("jant-collection-form") as
|
|
1716
|
+
| (HTMLElement & { loading: boolean })
|
|
1717
|
+
| null;
|
|
1718
|
+
if (formEl) formEl.loading = true;
|
|
1719
|
+
|
|
1720
|
+
try {
|
|
1721
|
+
const res = await fetch("/api/collections", {
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
headers: { "Content-Type": "application/json" },
|
|
1724
|
+
body: JSON.stringify(detail.data),
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1728
|
+
|
|
1729
|
+
const created = await res.json();
|
|
1730
|
+
const newCollection: ComposeCollection = {
|
|
1731
|
+
id: created.id,
|
|
1732
|
+
title: created.title,
|
|
1733
|
+
iconHtml: renderCollectionIcon(created.icon, { size: 16 }),
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
this.collections = [...this.collections, newCollection];
|
|
1737
|
+
this._collectionIds = [...this._collectionIds, created.id];
|
|
1738
|
+
this._addCollectionPanelOpen = false;
|
|
1739
|
+
showToast(this.labels.collectionFormLabels.submitLabel);
|
|
1740
|
+
} catch {
|
|
1741
|
+
showToast("Failed to create collection. Try again.", "error");
|
|
1742
|
+
} finally {
|
|
1743
|
+
if (formEl) formEl.loading = false;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
private _submitAddCollectionForm() {
|
|
1748
|
+
const form = this.querySelector<HTMLFormElement>(
|
|
1749
|
+
".compose-add-collection-panel form",
|
|
1750
|
+
);
|
|
1751
|
+
if (form) form.requestSubmit();
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
private _renderAddCollectionPanel() {
|
|
1755
|
+
if (!this._addCollectionPanelOpen) return nothing;
|
|
1756
|
+
|
|
1757
|
+
const initial = {
|
|
1758
|
+
title: "",
|
|
1759
|
+
slug: "",
|
|
1760
|
+
description: "",
|
|
1761
|
+
sortOrder: "newest",
|
|
1762
|
+
icon: "",
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
return html`
|
|
1766
|
+
<div class="compose-add-collection-panel">
|
|
1767
|
+
<div class="compose-alt-header">
|
|
1768
|
+
<button
|
|
1769
|
+
type="button"
|
|
1770
|
+
class="compose-attached-cancel"
|
|
1771
|
+
@click=${() => {
|
|
1772
|
+
this._addCollectionPanelOpen = false;
|
|
1773
|
+
}}
|
|
1774
|
+
>
|
|
1775
|
+
${this.labels.cancel}
|
|
1776
|
+
</button>
|
|
1777
|
+
<span class="compose-alt-title">${this.labels.addCollection}</span>
|
|
1778
|
+
<button
|
|
1779
|
+
type="button"
|
|
1780
|
+
class="compose-post-btn ml-auto"
|
|
1781
|
+
@click=${() => this._submitAddCollectionForm()}
|
|
1782
|
+
>
|
|
1783
|
+
${this.labels.done}
|
|
1784
|
+
</button>
|
|
1785
|
+
</div>
|
|
1786
|
+
<div class="flex-1 overflow-y-auto">
|
|
1787
|
+
<jant-collection-form
|
|
1788
|
+
class="compose-add-collection-form"
|
|
1789
|
+
.labels=${this.labels.collectionFormLabels}
|
|
1790
|
+
.initial=${initial}
|
|
1791
|
+
action="/api/collections"
|
|
1792
|
+
cancel-href="javascript:void(0)"
|
|
1793
|
+
@jant:collection-submit=${(e: Event) =>
|
|
1794
|
+
this._handleAddCollectionSubmit(e)}
|
|
1795
|
+
></jant-collection-form>
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
private _renderAttachedPanel() {
|
|
1802
|
+
if (!this._attachedPanelOpen) return nothing;
|
|
1803
|
+
|
|
1804
|
+
return html`
|
|
1805
|
+
<div class="compose-attached-panel">
|
|
1806
|
+
<div class="compose-alt-header">
|
|
1807
|
+
<button
|
|
1808
|
+
type="button"
|
|
1809
|
+
class="compose-attached-cancel"
|
|
1810
|
+
@click=${() => this._cancelAttachedPanel()}
|
|
1811
|
+
>
|
|
1812
|
+
${this.labels.cancel}
|
|
1813
|
+
</button>
|
|
1814
|
+
<span class="compose-alt-title">${this.labels.attachedText}</span>
|
|
1815
|
+
<button
|
|
1816
|
+
type="button"
|
|
1817
|
+
class="compose-post-btn ml-auto"
|
|
1818
|
+
@click=${() => this._doneAttachedPanel()}
|
|
1819
|
+
>
|
|
1820
|
+
${this.labels.done}
|
|
1821
|
+
</button>
|
|
1822
|
+
</div>
|
|
1823
|
+
<div class="flex-1 p-4 overflow-hidden flex flex-col">
|
|
1824
|
+
<div class="compose-attached-tiptap compose-tiptap-body"></div>
|
|
1825
|
+
</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
`;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private _renderAltPanel() {
|
|
1831
|
+
if (!this._altPanelOpen) return nothing;
|
|
1832
|
+
const attachment = this._getAltAttachment();
|
|
1833
|
+
if (!attachment) return nothing;
|
|
1834
|
+
|
|
1835
|
+
const category = getMediaCategory(attachment.file.type);
|
|
1836
|
+
|
|
1837
|
+
return html`
|
|
1838
|
+
<div class="compose-alt-panel">
|
|
1839
|
+
<div class="compose-alt-header">
|
|
1840
|
+
<button
|
|
1841
|
+
type="button"
|
|
1842
|
+
class="compose-attached-panel-back"
|
|
1843
|
+
@click=${() => this._closeAltPanel()}
|
|
1844
|
+
>
|
|
1845
|
+
<svg
|
|
1846
|
+
class="icon-fine"
|
|
1847
|
+
width="16"
|
|
1848
|
+
height="16"
|
|
1849
|
+
viewBox="0 0 16 16"
|
|
1850
|
+
fill="none"
|
|
1851
|
+
stroke="currentColor"
|
|
1852
|
+
stroke-width="1.5"
|
|
1853
|
+
stroke-linecap="round"
|
|
1854
|
+
stroke-linejoin="round"
|
|
1855
|
+
>
|
|
1856
|
+
<path d="M11 3L6 8l5 5" />
|
|
1857
|
+
</svg>
|
|
1858
|
+
</button>
|
|
1859
|
+
<span class="compose-alt-title">${this.labels.addAltTitle}</span>
|
|
1860
|
+
</div>
|
|
1861
|
+
<div class="compose-alt-preview">
|
|
1862
|
+
${category === "image"
|
|
1863
|
+
? html`<img
|
|
1864
|
+
src=${attachment.previewUrl}
|
|
1865
|
+
alt=""
|
|
1866
|
+
class="compose-alt-preview-img"
|
|
1867
|
+
/>`
|
|
1868
|
+
: category === "video"
|
|
1869
|
+
? html`<video
|
|
1870
|
+
src=${attachment.previewUrl}
|
|
1871
|
+
class="compose-alt-preview-img"
|
|
1872
|
+
preload="metadata"
|
|
1873
|
+
muted
|
|
1874
|
+
></video>`
|
|
1875
|
+
: html`<span class="text-sm text-muted-foreground"
|
|
1876
|
+
>${attachment.file.name}</span
|
|
1877
|
+
>`}
|
|
1878
|
+
</div>
|
|
1879
|
+
<div class="compose-alt-input-row">
|
|
1880
|
+
<input
|
|
1881
|
+
type="text"
|
|
1882
|
+
.value=${attachment.alt}
|
|
1883
|
+
@input=${(e: Event) => this._onAltInput(e)}
|
|
1884
|
+
class="compose-input compose-alt-input"
|
|
1885
|
+
placeholder=${this.labels.altPlaceholder}
|
|
1886
|
+
/>
|
|
1887
|
+
</div>
|
|
1888
|
+
<div class="compose-alt-footer">
|
|
1889
|
+
<span class="text-xs text-muted-foreground"
|
|
1890
|
+
>${this.labels.altHint}</span
|
|
1891
|
+
>
|
|
1892
|
+
<button
|
|
1893
|
+
type="button"
|
|
1894
|
+
class="compose-post-btn"
|
|
1895
|
+
@click=${() => this._closeAltPanel()}
|
|
1896
|
+
>
|
|
1897
|
+
${this.labels.done}
|
|
1898
|
+
</button>
|
|
1899
|
+
</div>
|
|
1900
|
+
</div>
|
|
1901
|
+
`;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
private _renderConfirmPanel() {
|
|
1905
|
+
if (!this._confirmPanelOpen) return nothing;
|
|
1906
|
+
|
|
1907
|
+
const isEdit = !!this._editPostId;
|
|
1908
|
+
const title = isEdit
|
|
1909
|
+
? this.labels.confirmEditTitle
|
|
1910
|
+
: this.labels.confirmCloseTitle;
|
|
1911
|
+
const subtitle = isEdit
|
|
1912
|
+
? this.labels.confirmEditSubtitle
|
|
1913
|
+
: this.labels.confirmCloseSubtitle;
|
|
1914
|
+
const saveLabel = isEdit
|
|
1915
|
+
? this.labels.confirmEditPublish
|
|
1916
|
+
: this.labels.confirmCloseSave;
|
|
1917
|
+
const discardLabel = isEdit
|
|
1918
|
+
? this.labels.confirmEditDiscard
|
|
1919
|
+
: this.labels.confirmCloseDiscard;
|
|
1920
|
+
|
|
1921
|
+
return html`
|
|
1922
|
+
<div class="compose-confirm-panel">
|
|
1923
|
+
<div class="compose-confirm-sheet">
|
|
1924
|
+
<div class="compose-confirm-header">
|
|
1925
|
+
<p class="compose-confirm-title">${title}</p>
|
|
1926
|
+
<p class="compose-confirm-subtitle">${subtitle}</p>
|
|
1927
|
+
</div>
|
|
1928
|
+
<button
|
|
1929
|
+
type="button"
|
|
1930
|
+
class="compose-confirm-action compose-confirm-save"
|
|
1931
|
+
@click=${() => this._handleConfirmSave()}
|
|
1932
|
+
>
|
|
1933
|
+
${saveLabel}
|
|
1934
|
+
</button>
|
|
1935
|
+
<button
|
|
1936
|
+
type="button"
|
|
1937
|
+
class="compose-confirm-action compose-confirm-discard"
|
|
1938
|
+
@click=${() => this._handleConfirmDiscard()}
|
|
1939
|
+
>
|
|
1940
|
+
${discardLabel}
|
|
1941
|
+
</button>
|
|
1942
|
+
<button
|
|
1943
|
+
type="button"
|
|
1944
|
+
class="compose-confirm-action compose-confirm-cancel"
|
|
1945
|
+
@click=${() => this.requestClose()}
|
|
1946
|
+
>
|
|
1947
|
+
${this.labels.confirmCloseCancel}
|
|
1948
|
+
</button>
|
|
1949
|
+
</div>
|
|
1950
|
+
</div>
|
|
1951
|
+
`;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
private _getSubmitLabel(): string {
|
|
1955
|
+
if (this._editPostId) return this.labels.update;
|
|
1956
|
+
if (this._replyToId) return this.labels.reply;
|
|
1957
|
+
return this.labels.post;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
private _submitWithVisibility(visibility: ComposeVisibility) {
|
|
1961
|
+
this._visibility = visibility;
|
|
1962
|
+
this._showVisibilityMenu = false;
|
|
1963
|
+
// Wait for state to update before submitting
|
|
1964
|
+
this.updateComplete.then(() => this._submit("published"));
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
private _renderPublishButton() {
|
|
1968
|
+
const spinner = html`<svg
|
|
1969
|
+
class="animate-spin size-4"
|
|
1970
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1971
|
+
viewBox="0 0 24 24"
|
|
1972
|
+
fill="none"
|
|
1973
|
+
stroke="currentColor"
|
|
1974
|
+
stroke-width="2"
|
|
1975
|
+
stroke-linecap="round"
|
|
1976
|
+
stroke-linejoin="round"
|
|
1977
|
+
role="status"
|
|
1978
|
+
>
|
|
1979
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
1980
|
+
</svg>`;
|
|
1981
|
+
|
|
1982
|
+
// In edit mode or reply mode, show a simple button (no visibility split)
|
|
1983
|
+
if (this._editPostId || this._replyToId) {
|
|
1984
|
+
return html`
|
|
1985
|
+
<button
|
|
1986
|
+
type="button"
|
|
1987
|
+
class="compose-post-btn"
|
|
1988
|
+
?disabled=${this._loading}
|
|
1989
|
+
@click=${() => this._submit("published")}
|
|
1990
|
+
>
|
|
1991
|
+
${this._loading ? spinner : nothing} ${this._getSubmitLabel()}
|
|
1992
|
+
</button>
|
|
1993
|
+
`;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
return html`
|
|
1997
|
+
<div class="compose-publish-group">
|
|
1998
|
+
${this._showVisibilityMenu
|
|
1999
|
+
? html`<div
|
|
2000
|
+
class="compose-dropdown-backdrop"
|
|
2001
|
+
@click=${() => {
|
|
2002
|
+
this._showVisibilityMenu = false;
|
|
2003
|
+
}}
|
|
2004
|
+
></div>`
|
|
2005
|
+
: nothing}
|
|
2006
|
+
<button
|
|
2007
|
+
type="button"
|
|
2008
|
+
class="compose-publish-main"
|
|
2009
|
+
?disabled=${this._loading}
|
|
2010
|
+
@click=${() => this._submit("published")}
|
|
2011
|
+
>
|
|
2012
|
+
${this._loading ? spinner : nothing} ${this._getSubmitLabel()}
|
|
2013
|
+
</button>
|
|
2014
|
+
<button
|
|
2015
|
+
type="button"
|
|
2016
|
+
class="compose-publish-toggle"
|
|
2017
|
+
?disabled=${this._loading}
|
|
2018
|
+
aria-haspopup="menu"
|
|
2019
|
+
aria-expanded=${this._showVisibilityMenu}
|
|
2020
|
+
@click=${() => {
|
|
2021
|
+
this._showVisibilityMenu = !this._showVisibilityMenu;
|
|
2022
|
+
}}
|
|
2023
|
+
>
|
|
2024
|
+
<svg
|
|
2025
|
+
width="14"
|
|
2026
|
+
height="14"
|
|
2027
|
+
viewBox="0 0 24 24"
|
|
2028
|
+
fill="none"
|
|
2029
|
+
stroke="currentColor"
|
|
2030
|
+
stroke-width="2.5"
|
|
2031
|
+
stroke-linecap="round"
|
|
2032
|
+
stroke-linejoin="round"
|
|
2033
|
+
>
|
|
2034
|
+
<path d="m6 9 6 6 6-6" />
|
|
2035
|
+
</svg>
|
|
2036
|
+
</button>
|
|
2037
|
+
${this._showVisibilityMenu
|
|
2038
|
+
? html`
|
|
2039
|
+
<div class="compose-dropdown" role="menu">
|
|
2040
|
+
<button
|
|
2041
|
+
type="button"
|
|
2042
|
+
class="compose-dropdown-item"
|
|
2043
|
+
role="menuitem"
|
|
2044
|
+
@click=${() => {
|
|
2045
|
+
this._featured = true;
|
|
2046
|
+
this._showVisibilityMenu = false;
|
|
2047
|
+
this.updateComplete.then(() => this._submit("published"));
|
|
2048
|
+
}}
|
|
2049
|
+
>
|
|
2050
|
+
<svg
|
|
2051
|
+
width="16"
|
|
2052
|
+
height="16"
|
|
2053
|
+
viewBox="0 0 24 24"
|
|
2054
|
+
fill="none"
|
|
2055
|
+
stroke="currentColor"
|
|
2056
|
+
stroke-width="2"
|
|
2057
|
+
stroke-linecap="round"
|
|
2058
|
+
stroke-linejoin="round"
|
|
2059
|
+
>
|
|
2060
|
+
<path
|
|
2061
|
+
d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
|
|
2062
|
+
/>
|
|
2063
|
+
</svg>
|
|
2064
|
+
${this.labels.publishFeatured}
|
|
2065
|
+
</button>
|
|
2066
|
+
<button
|
|
2067
|
+
type="button"
|
|
2068
|
+
class="compose-dropdown-item"
|
|
2069
|
+
role="menuitem"
|
|
2070
|
+
@click=${() => this._submitWithVisibility("unlisted")}
|
|
2071
|
+
>
|
|
2072
|
+
<svg
|
|
2073
|
+
width="16"
|
|
2074
|
+
height="16"
|
|
2075
|
+
viewBox="0 0 24 24"
|
|
2076
|
+
fill="none"
|
|
2077
|
+
stroke="currentColor"
|
|
2078
|
+
stroke-width="2"
|
|
2079
|
+
stroke-linecap="round"
|
|
2080
|
+
stroke-linejoin="round"
|
|
2081
|
+
>
|
|
2082
|
+
<path d="M9 17H7A5 5 0 0 1 7 7h2" />
|
|
2083
|
+
<path d="M15 7h2a5 5 0 1 1 0 10h-2" />
|
|
2084
|
+
<line x1="8" x2="16" y1="12" y2="12" />
|
|
2085
|
+
</svg>
|
|
2086
|
+
${this.labels.publishUnlisted}
|
|
2087
|
+
</button>
|
|
2088
|
+
<button
|
|
2089
|
+
type="button"
|
|
2090
|
+
class="compose-dropdown-item"
|
|
2091
|
+
role="menuitem"
|
|
2092
|
+
@click=${() => this._submitWithVisibility("private")}
|
|
2093
|
+
>
|
|
2094
|
+
<svg
|
|
2095
|
+
width="16"
|
|
2096
|
+
height="16"
|
|
2097
|
+
viewBox="0 0 24 24"
|
|
2098
|
+
fill="none"
|
|
2099
|
+
stroke="currentColor"
|
|
2100
|
+
stroke-width="2"
|
|
2101
|
+
stroke-linecap="round"
|
|
2102
|
+
stroke-linejoin="round"
|
|
2103
|
+
>
|
|
2104
|
+
<path
|
|
2105
|
+
d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"
|
|
2106
|
+
/>
|
|
2107
|
+
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
|
2108
|
+
<path
|
|
2109
|
+
d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"
|
|
2110
|
+
/>
|
|
2111
|
+
<path d="m2 2 20 20" />
|
|
2112
|
+
</svg>
|
|
2113
|
+
${this.labels.publishPrivate}
|
|
2114
|
+
</button>
|
|
2115
|
+
</div>
|
|
2116
|
+
`
|
|
2117
|
+
: nothing}
|
|
2118
|
+
</div>
|
|
2119
|
+
`;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
render() {
|
|
2123
|
+
const isReply = !!(this._replyToId && this._replyToData);
|
|
2124
|
+
const editor = html`<jant-compose-editor
|
|
2125
|
+
.format=${this._format}
|
|
2126
|
+
.labels=${this.labels}
|
|
2127
|
+
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
2128
|
+
></jant-compose-editor>`;
|
|
2129
|
+
|
|
2130
|
+
return html`
|
|
2131
|
+
<div
|
|
2132
|
+
class=${classMap({
|
|
2133
|
+
"compose-dialog-inner": true,
|
|
2134
|
+
"compose-dialog-inner-page": this.pageMode,
|
|
2135
|
+
})}
|
|
2136
|
+
>
|
|
2137
|
+
${this._renderHeader()}
|
|
2138
|
+
${isReply
|
|
2139
|
+
? html`
|
|
2140
|
+
<div class="compose-thread-layout">
|
|
2141
|
+
${this._renderReplyContext()}
|
|
2142
|
+
<div class="compose-editor-row">
|
|
2143
|
+
<div class="compose-thread-dot"></div>
|
|
2144
|
+
${editor}
|
|
2145
|
+
</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
`
|
|
2148
|
+
: editor}
|
|
2149
|
+
|
|
2150
|
+
<div class="compose-action-row">
|
|
2151
|
+
${this._renderCollectionSelector()} ${this._renderPublishButton()}
|
|
2152
|
+
</div>
|
|
2153
|
+
${this._renderAttachedPanel()} ${this._renderAltPanel()}
|
|
2154
|
+
${this._renderDraftsPanel()} ${this._renderConfirmPanel()}
|
|
2155
|
+
</div>
|
|
2156
|
+
${this._renderAddCollectionPanel()}
|
|
2157
|
+
`;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
customElements.define("jant-compose-dialog", JantComposeDialog);
|