@jant/core 0.6.15 → 0.7.0
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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { LitElement, html, nothing } from "lit";
|
|
12
|
+
import type { TemplateResult } from "lit";
|
|
12
13
|
import { classMap } from "lit/directives/class-map.js";
|
|
13
14
|
import { ifDefined } from "lit/directives/if-defined.js";
|
|
14
15
|
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
ComposeFormat,
|
|
27
28
|
ComposeLabels,
|
|
28
29
|
ComposeAttachment,
|
|
30
|
+
ComposeRowStatus,
|
|
29
31
|
AttachedTextItem,
|
|
30
32
|
ComposeEditorSelection,
|
|
31
33
|
ComposeFullscreenOpenDetail,
|
|
@@ -35,11 +37,7 @@ import {
|
|
|
35
37
|
promoteLeadingH1Title as getLeadingH1TitlePromotion,
|
|
36
38
|
type LeadingH1TitlePromotion,
|
|
37
39
|
} from "./compose-title-from-heading.js";
|
|
38
|
-
import {
|
|
39
|
-
UPLOAD_ACCEPT,
|
|
40
|
-
getMediaCategory,
|
|
41
|
-
validateUploadFile,
|
|
42
|
-
} from "../../lib/upload.js";
|
|
40
|
+
import { getMediaCategory, validateUploadFile } from "../../lib/upload.js";
|
|
43
41
|
import type { MediaCategory } from "../../lib/upload.js";
|
|
44
42
|
import {
|
|
45
43
|
DECORATIVE_QUOTE_MARK_SVG_CONTENT,
|
|
@@ -69,6 +67,13 @@ interface ComposeFilePickerCloseDetail {
|
|
|
69
67
|
cancelled: boolean;
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Which control a reordered attachment hands focus back to. The strip renders
|
|
72
|
+
* positionally, so without this the focused DOM slot silently retargets a
|
|
73
|
+
* different attachment after every move.
|
|
74
|
+
*/
|
|
75
|
+
type AttachmentFocusTarget = "handle" | "move";
|
|
76
|
+
|
|
72
77
|
const COMPOSE_TOOLBAR_ICONS = {
|
|
73
78
|
media: `
|
|
74
79
|
<rect x="2.75" y="3" width="12.5" height="11.25" rx="3" />
|
|
@@ -97,43 +102,44 @@ const COMPOSE_TOOLBAR_ICONS = {
|
|
|
97
102
|
/>
|
|
98
103
|
<path
|
|
99
104
|
d="m9 1.95 2.08 4.21 4.65.67-3.36 3.29.8 4.63L9 12.55l-4.17 2.2.8-4.63-3.36-3.29 4.65-.67z"
|
|
100
|
-
stroke-width="1.6"
|
|
101
105
|
/>
|
|
102
106
|
`,
|
|
107
|
+
/* The one filled glyph in the row, so its bars are cut to the weight the
|
|
108
|
+
stroked icons read at — a 2.05 bar beside a 1.42 stroke looks bold. */
|
|
103
109
|
title: `
|
|
104
110
|
<rect
|
|
105
111
|
x="3.35"
|
|
106
|
-
y="3.
|
|
112
|
+
y="3.37"
|
|
107
113
|
width="11.3"
|
|
108
|
-
height="
|
|
109
|
-
rx="0.
|
|
114
|
+
height="1.72"
|
|
115
|
+
rx="0.6"
|
|
110
116
|
fill="currentColor"
|
|
111
117
|
stroke="none"
|
|
112
118
|
/>
|
|
113
119
|
<rect
|
|
114
|
-
x="7.
|
|
120
|
+
x="7.99"
|
|
115
121
|
y="4.6"
|
|
116
|
-
width="2.
|
|
122
|
+
width="2.02"
|
|
117
123
|
height="9.45"
|
|
118
|
-
rx="0.
|
|
124
|
+
rx="0.68"
|
|
119
125
|
fill="currentColor"
|
|
120
126
|
stroke="none"
|
|
121
127
|
/>
|
|
122
128
|
<rect
|
|
123
129
|
x="6.75"
|
|
124
|
-
y="13.
|
|
130
|
+
y="13.32"
|
|
125
131
|
width="4.5"
|
|
126
|
-
height="
|
|
127
|
-
rx="0.
|
|
132
|
+
height="0.95"
|
|
133
|
+
rx="0.48"
|
|
128
134
|
fill="currentColor"
|
|
129
135
|
stroke="none"
|
|
130
136
|
/>
|
|
131
137
|
`,
|
|
132
138
|
fullscreen: `
|
|
133
|
-
<path d="M5.85 3H3v2.85"
|
|
134
|
-
<path d="M12.15 3H15v2.85"
|
|
135
|
-
<path d="M3 12.15V15h2.85"
|
|
136
|
-
<path d="M15 12.15V15h-2.85"
|
|
139
|
+
<path d="M5.85 3H3v2.85" />
|
|
140
|
+
<path d="M12.15 3H15v2.85" />
|
|
141
|
+
<path d="M3 12.15V15h2.85" />
|
|
142
|
+
<path d="M15 12.15V15h-2.85" />
|
|
137
143
|
`,
|
|
138
144
|
} as const;
|
|
139
145
|
|
|
@@ -147,7 +153,6 @@ function renderComposeToolbarIcon(
|
|
|
147
153
|
viewBox="0 0 18 18"
|
|
148
154
|
fill="none"
|
|
149
155
|
stroke="currentColor"
|
|
150
|
-
stroke-width="1.55"
|
|
151
156
|
stroke-linecap="round"
|
|
152
157
|
stroke-linejoin="round"
|
|
153
158
|
aria-hidden="true"
|
|
@@ -189,6 +194,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
189
194
|
threadItem: { type: Boolean, attribute: "thread-item" },
|
|
190
195
|
removable: { type: Boolean },
|
|
191
196
|
inlineFormat: { type: Boolean, attribute: "inline-format" },
|
|
197
|
+
titleByDefault: { type: Boolean, attribute: "title-by-default" },
|
|
198
|
+
positionLabel: { type: String, attribute: "position-label" },
|
|
199
|
+
badgeLabel: { type: String, attribute: "badge-label" },
|
|
200
|
+
headerExtra: { attribute: false },
|
|
192
201
|
slashCommandDiscovered: { type: Boolean },
|
|
193
202
|
_title: { state: true },
|
|
194
203
|
_bodyJson: { state: true },
|
|
@@ -215,6 +224,29 @@ export class JantComposeEditor extends LitElement {
|
|
|
215
224
|
declare threadItem: boolean;
|
|
216
225
|
declare removable: boolean;
|
|
217
226
|
declare inlineFormat: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Whether a note's title field starts visible. A post that opens a thread is
|
|
229
|
+
* usually the one worth naming, so it defaults on there; a reply or a
|
|
230
|
+
* continuation post carries the thread's title already, so it defaults off.
|
|
231
|
+
* On a new single post the owner narrows it further with the author's own
|
|
232
|
+
* remembered answer. Either way the `T` tool overrides it, and a post that
|
|
233
|
+
* arrives with a title always shows the field.
|
|
234
|
+
*/
|
|
235
|
+
declare titleByDefault: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* "1/3"-style position marker shown before the format selector. Empty for a
|
|
238
|
+
* single post, where there is no "which one" to answer.
|
|
239
|
+
*/
|
|
240
|
+
declare positionLabel: string;
|
|
241
|
+
/** Status marker for the header row, e.g. "Editing". Mutually exclusive with
|
|
242
|
+
* `positionLabel` in practice — a thread post is never an edit. */
|
|
243
|
+
declare badgeLabel: string;
|
|
244
|
+
/**
|
|
245
|
+
* Owner-supplied content for the right side of the format header — the
|
|
246
|
+
* dialog's per-post date/permalink control. Passed in rather than built here
|
|
247
|
+
* because publish metadata belongs to the dialog, not to the editor.
|
|
248
|
+
*/
|
|
249
|
+
declare headerExtra: unknown;
|
|
218
250
|
declare slashCommandDiscovered: boolean;
|
|
219
251
|
declare _title: string;
|
|
220
252
|
declare _bodyJson: JSONContent | null;
|
|
@@ -243,7 +275,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
243
275
|
private _emojiContainer: HTMLElement | null = null;
|
|
244
276
|
private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
|
|
245
277
|
private _onDocClickBound = this._onDocumentClick.bind(this);
|
|
246
|
-
private _scrollBufferApplied = false;
|
|
247
278
|
private _filePickerCleanup: (() => void) | null = null;
|
|
248
279
|
private _suppressAttachedTextOpenUntil = 0;
|
|
249
280
|
/**
|
|
@@ -254,12 +285,24 @@ export class JantComposeEditor extends LitElement {
|
|
|
254
285
|
* to run this cycle.
|
|
255
286
|
*/
|
|
256
287
|
private _suppressContentChangedOnce = false;
|
|
288
|
+
/**
|
|
289
|
+
* Set when the title toggle arrives with the post rather than from the
|
|
290
|
+
* owner's default — a handed-over editor keeps the answer it was given
|
|
291
|
+
* instead of having the first update reseed it from `titleByDefault`.
|
|
292
|
+
*/
|
|
293
|
+
#showTitleHandedOver = false;
|
|
257
294
|
#inlineImageUploadGeneration = 0;
|
|
258
295
|
#inlineImageUploadPromises = new Set<Promise<void>>();
|
|
259
296
|
#rehostFailureCount = 0;
|
|
260
297
|
#rehostFailureNoticeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
261
298
|
#sortable: { destroy(): void } | null = null;
|
|
262
299
|
#revertNextSibling: globalThis.Node | null = null;
|
|
300
|
+
#focusAfterMove: {
|
|
301
|
+
clientId: string;
|
|
302
|
+
direction: -1 | 1;
|
|
303
|
+
focusTarget: AttachmentFocusTarget;
|
|
304
|
+
} | null = null;
|
|
305
|
+
#coarsePointerQuery: globalThis.MediaQueryList | null | undefined;
|
|
263
306
|
|
|
264
307
|
createRenderRoot() {
|
|
265
308
|
return this;
|
|
@@ -273,6 +316,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
273
316
|
this.threadItem = false;
|
|
274
317
|
this.removable = false;
|
|
275
318
|
this.inlineFormat = false;
|
|
319
|
+
this.titleByDefault = true;
|
|
320
|
+
this.positionLabel = "";
|
|
321
|
+
this.badgeLabel = "";
|
|
322
|
+
this.headerExtra = null;
|
|
276
323
|
this.slashCommandDiscovered = false;
|
|
277
324
|
this._title = "";
|
|
278
325
|
this._bodyJson = null;
|
|
@@ -280,7 +327,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
280
327
|
this._quoteText = "";
|
|
281
328
|
this._quoteAuthor = "";
|
|
282
329
|
this._rating = 0;
|
|
283
|
-
this._showTitle =
|
|
330
|
+
this._showTitle = true;
|
|
284
331
|
this._showRating = false;
|
|
285
332
|
this._attachedTexts = [];
|
|
286
333
|
this._attachments = [];
|
|
@@ -307,6 +354,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
307
354
|
this.addEventListener("dragover", this._onDragOver, true);
|
|
308
355
|
this.addEventListener("dragleave", this._onDragLeave);
|
|
309
356
|
this.addEventListener("drop", this._onDrop);
|
|
357
|
+
this.#coarsePointer()?.addEventListener(
|
|
358
|
+
"change",
|
|
359
|
+
this._onPointerTypeChange,
|
|
360
|
+
);
|
|
310
361
|
}
|
|
311
362
|
|
|
312
363
|
disconnectedCallback() {
|
|
@@ -331,6 +382,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
331
382
|
this._emojiPickerEl = null;
|
|
332
383
|
this._filePickerCleanup?.();
|
|
333
384
|
this._filePickerCleanup = null;
|
|
385
|
+
this.#coarsePointer()?.removeEventListener(
|
|
386
|
+
"change",
|
|
387
|
+
this._onPointerTypeChange,
|
|
388
|
+
);
|
|
334
389
|
}
|
|
335
390
|
|
|
336
391
|
// Tracks dragenter/dragleave nesting so the highlight only clears when the
|
|
@@ -778,9 +833,90 @@ export class JantComposeEditor extends LitElement {
|
|
|
778
833
|
}
|
|
779
834
|
}
|
|
780
835
|
|
|
836
|
+
/**
|
|
837
|
+
* What this row contributes to the composer's submit state.
|
|
838
|
+
*
|
|
839
|
+
* The editor owns these fields, so it owns the answer: the dialog reduces
|
|
840
|
+
* `getStatus()` over the rows it has rather than reading editors back out of
|
|
841
|
+
* the DOM, which during a render still holds the row it is about to remove.
|
|
842
|
+
*
|
|
843
|
+
* @returns Whether the row holds anything worth keeping, and whether it is
|
|
844
|
+
* complete enough to publish
|
|
845
|
+
* @example
|
|
846
|
+
* editor.getStatus(); // { hasContent: true, publishable: false } — link, no URL yet
|
|
847
|
+
*/
|
|
848
|
+
getStatus(): ComposeRowStatus {
|
|
849
|
+
const data = this.getData();
|
|
850
|
+
const hasContent =
|
|
851
|
+
!!data.body ||
|
|
852
|
+
!!data.title.trim() ||
|
|
853
|
+
!!data.url.trim() ||
|
|
854
|
+
!!data.quoteText.trim() ||
|
|
855
|
+
!!data.quoteAuthor.trim() ||
|
|
856
|
+
data.rating > 0 ||
|
|
857
|
+
data.attachments.length > 0 ||
|
|
858
|
+
data.attachedTexts.length > 0;
|
|
859
|
+
const fieldsValid =
|
|
860
|
+
!this.getUrlValidationMessage() && !this.getLinkTitleValidationMessage();
|
|
861
|
+
|
|
862
|
+
let publishable: boolean;
|
|
863
|
+
switch (this.format) {
|
|
864
|
+
// A link is its URL and title, both of which the validators already
|
|
865
|
+
// require — nothing else has to be filled in.
|
|
866
|
+
case "link":
|
|
867
|
+
publishable = fieldsValid;
|
|
868
|
+
break;
|
|
869
|
+
case "quote":
|
|
870
|
+
publishable = fieldsValid && !!data.quoteText.trim();
|
|
871
|
+
break;
|
|
872
|
+
default:
|
|
873
|
+
publishable = fieldsValid && hasContent;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return { hasContent, publishable };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#lastStatus: ComposeRowStatus | null = null;
|
|
880
|
+
|
|
881
|
+
/** Tell the owner when — and only when — this row's answer changes. */
|
|
882
|
+
#notifyStatus() {
|
|
883
|
+
const status = this.getStatus();
|
|
884
|
+
if (
|
|
885
|
+
this.#lastStatus?.hasContent === status.hasContent &&
|
|
886
|
+
this.#lastStatus?.publishable === status.publishable
|
|
887
|
+
) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
this.#lastStatus = status;
|
|
891
|
+
this.dispatchEvent(
|
|
892
|
+
new CustomEvent<ComposeRowStatus>("jant:compose-status", {
|
|
893
|
+
bubbles: true,
|
|
894
|
+
detail: status,
|
|
895
|
+
}),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Set the title toggle's default and apply it now.
|
|
901
|
+
*
|
|
902
|
+
* `titleByDefault` alone only takes effect on the first update or on
|
|
903
|
+
* `reset()`, and an owner that resets before Lit re-renders would seed from
|
|
904
|
+
* the previous session's value. An owner that knows the default up front
|
|
905
|
+
* calls this instead.
|
|
906
|
+
*
|
|
907
|
+
* @param show - Whether a note's title field starts visible
|
|
908
|
+
* @example
|
|
909
|
+
* editor.setTitleDefault(false); // next new note opens without a title field
|
|
910
|
+
*/
|
|
911
|
+
setTitleDefault(show: boolean) {
|
|
912
|
+
this.titleByDefault = show;
|
|
913
|
+
this._showTitle = show;
|
|
914
|
+
}
|
|
915
|
+
|
|
781
916
|
reset() {
|
|
782
917
|
this.#clearPendingInlineImageUploads();
|
|
783
918
|
this._title = "";
|
|
919
|
+
this._showTitle = this.titleByDefault;
|
|
784
920
|
this._bodyJson = null;
|
|
785
921
|
this._editor?.commands.clearContent();
|
|
786
922
|
this._lastEditorSelection = null;
|
|
@@ -788,7 +924,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
788
924
|
this._quoteText = "";
|
|
789
925
|
this._quoteAuthor = "";
|
|
790
926
|
this._rating = 0;
|
|
791
|
-
this._showTitle = false;
|
|
792
927
|
this._showRating = false;
|
|
793
928
|
this._attachedTexts = [];
|
|
794
929
|
// Revoke preview URLs before clearing
|
|
@@ -811,9 +946,12 @@ export class JantComposeEditor extends LitElement {
|
|
|
811
946
|
status: ComposeAttachment["status"],
|
|
812
947
|
mediaId: string | null,
|
|
813
948
|
error: string | null,
|
|
949
|
+
remoteUrl?: string | null,
|
|
814
950
|
) {
|
|
815
951
|
this._attachments = this._attachments.map((a) =>
|
|
816
|
-
a.clientId === clientId
|
|
952
|
+
a.clientId === clientId
|
|
953
|
+
? { ...a, status, mediaId, error, remoteUrl: remoteUrl ?? a.remoteUrl }
|
|
954
|
+
: a,
|
|
817
955
|
);
|
|
818
956
|
}
|
|
819
957
|
|
|
@@ -911,20 +1049,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
911
1049
|
return this._title.trim() ? null : this.labels.linkTitleRequired;
|
|
912
1050
|
}
|
|
913
1051
|
|
|
914
|
-
private _getInlineUrlValidationMessage(): string | null {
|
|
915
|
-
if (this.format === "link" && !this._url.trim()) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
return this.getUrlValidationMessage();
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
private _getInlineLinkTitleValidationMessage(): string | null {
|
|
922
|
-
if (this.format !== "link" || !this._title.trim()) {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
|
-
return this.getLinkTitleValidationMessage();
|
|
926
|
-
}
|
|
927
|
-
|
|
928
1052
|
revealUrlValidation(): string | null {
|
|
929
1053
|
this._showUrlValidation = true;
|
|
930
1054
|
return this.getUrlValidationMessage();
|
|
@@ -978,7 +1102,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
978
1102
|
toolbarMode: "compose",
|
|
979
1103
|
onUpdate: (json) => {
|
|
980
1104
|
this._bodyJson = json;
|
|
981
|
-
this._ensureScrollBuffer();
|
|
982
1105
|
this.promoteLeadingH1Title();
|
|
983
1106
|
hideSlashCommandHint(this);
|
|
984
1107
|
},
|
|
@@ -1009,36 +1132,12 @@ export class JantComposeEditor extends LitElement {
|
|
|
1009
1132
|
tableControlLabels: this.labels.tableControls,
|
|
1010
1133
|
});
|
|
1011
1134
|
this._lastEditorSelection = this._readEditorSelection();
|
|
1012
|
-
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const isPageMode = !!this.closest(".compose-page-shell");
|
|
1019
|
-
if (!isPageMode) {
|
|
1020
|
-
const last = dom.lastElementChild as HTMLElement | null;
|
|
1021
|
-
const contentH = last ? last.offsetTop + last.offsetHeight : 0;
|
|
1022
|
-
const buffer = this.format !== "note" ? 60 : 120;
|
|
1023
|
-
dom.style.minHeight = `${contentH + buffer}px`;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* One-time: adds bottom padding for scroll buffer once the
|
|
1029
|
-
* compose-body starts scrolling. Since the dialog is already at
|
|
1030
|
-
* max-height by that point, the extra padding doesn't grow it.
|
|
1031
|
-
*/
|
|
1032
|
-
private _ensureScrollBuffer() {
|
|
1033
|
-
if (this._scrollBufferApplied) return;
|
|
1034
|
-
const dom = this._editor?.view?.dom as HTMLElement | undefined;
|
|
1035
|
-
if (!dom) return;
|
|
1036
|
-
const body = this.querySelector(".compose-body") as HTMLElement | null;
|
|
1037
|
-
if (!body) return;
|
|
1038
|
-
if (body.scrollHeight > body.clientHeight + 20) {
|
|
1039
|
-
dom.style.paddingBottom = "40px";
|
|
1040
|
-
this._scrollBufferApplied = true;
|
|
1041
|
-
}
|
|
1135
|
+
// An editor built from existing content — after a format switch, or on a
|
|
1136
|
+
// replacement element that took over a post — inherits the uploads still
|
|
1137
|
+
// working on that content. Without this the `blob:` placeholder is left
|
|
1138
|
+
// with nothing watching it: the finished upload has no editor to write
|
|
1139
|
+
// into, and publishing puts the blob URL in the post.
|
|
1140
|
+
this.adoptPendingUploads();
|
|
1042
1141
|
}
|
|
1043
1142
|
|
|
1044
1143
|
private _destroyEditor() {
|
|
@@ -1061,6 +1160,17 @@ export class JantComposeEditor extends LitElement {
|
|
|
1061
1160
|
"_attachmentOrder",
|
|
1062
1161
|
]);
|
|
1063
1162
|
|
|
1163
|
+
protected willUpdate(changed: Map<string, unknown>) {
|
|
1164
|
+
super.willUpdate(changed);
|
|
1165
|
+
|
|
1166
|
+
// `titleByDefault` is an owner-supplied prop, so it is only known once the
|
|
1167
|
+
// first update runs. Seed the toggle from it there and leave it alone after
|
|
1168
|
+
// that — from then on the value belongs to the user (and to `reset()`).
|
|
1169
|
+
if (!this.hasUpdated && !this.#showTitleHandedOver) {
|
|
1170
|
+
this._showTitle = this.titleByDefault;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1064
1174
|
protected updated(changed: Map<string, unknown>) {
|
|
1065
1175
|
super.updated(changed);
|
|
1066
1176
|
|
|
@@ -1096,8 +1206,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
1096
1206
|
this.#sortable?.destroy();
|
|
1097
1207
|
this.#sortable = null;
|
|
1098
1208
|
}
|
|
1209
|
+
this.#restoreAttachmentFocus();
|
|
1099
1210
|
}
|
|
1100
1211
|
|
|
1212
|
+
// Every field that can change this row's answer is reactive, so `updated()`
|
|
1213
|
+
// is where the owner hears about it — including the ones a user edit never
|
|
1214
|
+
// touches, like an upload finishing.
|
|
1215
|
+
this.#notifyStatus();
|
|
1216
|
+
|
|
1101
1217
|
// Notify parent dialog of content changes for draft auto-save. A format
|
|
1102
1218
|
// conversion writes content fields too, but it's not a user edit, so skip
|
|
1103
1219
|
// the notification once when asked.
|
|
@@ -1158,6 +1274,63 @@ export class JantComposeEditor extends LitElement {
|
|
|
1158
1274
|
this._bodyJson = fields.bodyJson;
|
|
1159
1275
|
}
|
|
1160
1276
|
|
|
1277
|
+
/**
|
|
1278
|
+
* Take over everything another editor holds for its post.
|
|
1279
|
+
*
|
|
1280
|
+
* Entering or leaving thread mode renders a different editor element for the
|
|
1281
|
+
* same post, and the element being replaced has the only copy of what was
|
|
1282
|
+
* written. Every field that belongs to the post moves here, in this one
|
|
1283
|
+
* place: a caller picking fields out by hand drops whichever one it forgets,
|
|
1284
|
+
* which is how a quote's source link used to vanish on "Add to thread".
|
|
1285
|
+
* Fields hidden by the current format move too, so switching back still
|
|
1286
|
+
* finds them.
|
|
1287
|
+
*
|
|
1288
|
+
* Safe before or after this editor's first render, and on a `previous` that
|
|
1289
|
+
* has already left the DOM — its TipTap instance is gone by then, but
|
|
1290
|
+
* `_bodyJson` tracks every edit.
|
|
1291
|
+
*
|
|
1292
|
+
* @param previous - The editor this one replaces
|
|
1293
|
+
* @example
|
|
1294
|
+
* await dialog.updateComplete;
|
|
1295
|
+
* threadRootEditor.takeOver(singlePostEditor);
|
|
1296
|
+
*/
|
|
1297
|
+
takeOver(previous: JantComposeEditor) {
|
|
1298
|
+
this._title = previous._title;
|
|
1299
|
+
this._showTitle = previous._showTitle;
|
|
1300
|
+
this.#showTitleHandedOver = true;
|
|
1301
|
+
this._bodyJson = previous._editor?.getJSON() ?? previous._bodyJson;
|
|
1302
|
+
this._url = previous._url;
|
|
1303
|
+
this._quoteText = previous._quoteText;
|
|
1304
|
+
this._quoteAuthor = previous._quoteAuthor;
|
|
1305
|
+
this._rating = previous._rating;
|
|
1306
|
+
this._showRating = previous._showRating;
|
|
1307
|
+
this._attachedTexts = [...previous._attachedTexts];
|
|
1308
|
+
this._attachments = [...previous._attachments];
|
|
1309
|
+
this._attachmentOrder = [...previous._attachmentOrder];
|
|
1310
|
+
this._lastEditorSelection = previous.getEditorSelection();
|
|
1311
|
+
// Before the first render there is no TipTap instance yet; `_initEditor`
|
|
1312
|
+
// builds it from `_bodyJson` and adopts the uploads then.
|
|
1313
|
+
if (this._editor) {
|
|
1314
|
+
this._editor.commands.setContent(
|
|
1315
|
+
this._bodyJson ?? { type: "doc", content: [{ type: "paragraph" }] },
|
|
1316
|
+
);
|
|
1317
|
+
this.adoptPendingUploads();
|
|
1318
|
+
}
|
|
1319
|
+
this.#notifyStatus();
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Whether this editor holds the attachment an upload is working on.
|
|
1324
|
+
*
|
|
1325
|
+
* @param clientId - The attachment's client-side id
|
|
1326
|
+
* @returns True when the attachment strip includes it
|
|
1327
|
+
* @example
|
|
1328
|
+
* editor.hasAttachment(clientId); // false once the post moved to another editor
|
|
1329
|
+
*/
|
|
1330
|
+
hasAttachment(clientId: string): boolean {
|
|
1331
|
+
return this._attachments.some((a) => a.clientId === clientId);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1161
1334
|
/** Pre-fill all fields for edit mode or draft restore */
|
|
1162
1335
|
populate(data: {
|
|
1163
1336
|
format: string;
|
|
@@ -1171,6 +1344,8 @@ export class JantComposeEditor extends LitElement {
|
|
|
1171
1344
|
showRating?: boolean;
|
|
1172
1345
|
media?: Array<{
|
|
1173
1346
|
id: string;
|
|
1347
|
+
/** Preserved so a restored draft's attachmentOrder still maps by clientId */
|
|
1348
|
+
clientId?: string;
|
|
1174
1349
|
previewUrl: string;
|
|
1175
1350
|
posterUrl?: string | null;
|
|
1176
1351
|
alt?: string;
|
|
@@ -1196,6 +1371,8 @@ export class JantComposeEditor extends LitElement {
|
|
|
1196
1371
|
this._rating = data.rating;
|
|
1197
1372
|
this._showRating = true;
|
|
1198
1373
|
}
|
|
1374
|
+
// A post that already has a title shows the field whatever the default is —
|
|
1375
|
+
// otherwise editing a titled reply would silently drop its title.
|
|
1199
1376
|
if (data.showTitle !== undefined) this._showTitle = data.showTitle;
|
|
1200
1377
|
else if (data.title && data.format === "note") this._showTitle = true;
|
|
1201
1378
|
if (data.showRating !== undefined) this._showRating = data.showRating;
|
|
@@ -1217,13 +1394,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
1217
1394
|
// Convert media attachments to ComposeAttachment[] with status "done"
|
|
1218
1395
|
if (data.media?.length) {
|
|
1219
1396
|
const attachments = data.media.map((m) => ({
|
|
1220
|
-
clientId: randomUUID(),
|
|
1397
|
+
clientId: m.clientId ?? randomUUID(),
|
|
1221
1398
|
file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
|
|
1222
1399
|
previewUrl: m.previewUrl,
|
|
1223
1400
|
posterUrl: m.posterUrl ?? null,
|
|
1224
1401
|
status: "done" as const,
|
|
1225
1402
|
progress: null,
|
|
1226
1403
|
mediaId: m.id,
|
|
1404
|
+
remoteUrl: m.previewUrl,
|
|
1227
1405
|
alt: m.alt ?? "",
|
|
1228
1406
|
error: null,
|
|
1229
1407
|
summary: m.summary ?? null,
|
|
@@ -1281,6 +1459,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1281
1459
|
|
|
1282
1460
|
this._showUrlValidation = false;
|
|
1283
1461
|
this._showLinkTitleValidation = false;
|
|
1462
|
+
this.#notifyStatus();
|
|
1284
1463
|
}
|
|
1285
1464
|
|
|
1286
1465
|
/** Updates editor content and title from fullscreen close */
|
|
@@ -1304,6 +1483,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
1304
1483
|
);
|
|
1305
1484
|
}
|
|
1306
1485
|
this._lastEditorSelection = selection ?? this._readEditorSelection();
|
|
1486
|
+
// Content handed in from outside has to be visible to the owner in the same
|
|
1487
|
+
// tick: fullscreen's ⌘↵ hands the text back and publishes on the next line,
|
|
1488
|
+
// with no render in between to carry the news.
|
|
1489
|
+
this.#notifyStatus();
|
|
1307
1490
|
}
|
|
1308
1491
|
|
|
1309
1492
|
private static SUMMARY_LENGTH = 100;
|
|
@@ -1332,7 +1515,11 @@ export class JantComposeEditor extends LitElement {
|
|
|
1332
1515
|
);
|
|
1333
1516
|
}
|
|
1334
1517
|
|
|
1335
|
-
private _moveAttachment(
|
|
1518
|
+
private _moveAttachment(
|
|
1519
|
+
clientId: string,
|
|
1520
|
+
direction: -1 | 1,
|
|
1521
|
+
focusTarget: AttachmentFocusTarget = "handle",
|
|
1522
|
+
) {
|
|
1336
1523
|
const index = this._attachmentOrder.indexOf(clientId);
|
|
1337
1524
|
const nextIndex = index + direction;
|
|
1338
1525
|
if (
|
|
@@ -1348,9 +1535,50 @@ export class JantComposeEditor extends LitElement {
|
|
|
1348
1535
|
if (!item) return;
|
|
1349
1536
|
nextOrder.splice(nextIndex, 0, item);
|
|
1350
1537
|
this._attachmentOrder = nextOrder;
|
|
1538
|
+
this.#focusAfterMove = { clientId, direction, focusTarget };
|
|
1351
1539
|
this.#scrollAttachmentIntoView(clientId);
|
|
1352
1540
|
}
|
|
1353
1541
|
|
|
1542
|
+
/**
|
|
1543
|
+
* Returns focus to the attachment that just moved.
|
|
1544
|
+
*
|
|
1545
|
+
* Lit reuses the strip's DOM slots, so the element that held focus now
|
|
1546
|
+
* renders a different attachment. Called from `updated()`, once the new
|
|
1547
|
+
* order is in the DOM.
|
|
1548
|
+
*/
|
|
1549
|
+
#restoreAttachmentFocus() {
|
|
1550
|
+
const pending = this.#focusAfterMove;
|
|
1551
|
+
if (!pending) return;
|
|
1552
|
+
this.#focusAfterMove = null;
|
|
1553
|
+
|
|
1554
|
+
const item = this.querySelector<HTMLElement>(
|
|
1555
|
+
`[data-attachment-id="${pending.clientId}"]`,
|
|
1556
|
+
);
|
|
1557
|
+
if (!item) return;
|
|
1558
|
+
|
|
1559
|
+
if (pending.focusTarget === "handle") {
|
|
1560
|
+
item
|
|
1561
|
+
.querySelector<HTMLElement>("[data-attachment-sortable]")
|
|
1562
|
+
?.focus({ preventScroll: true });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// The pressed button turns disabled at either end of the strip, and a
|
|
1567
|
+
// disabled button cannot hold focus — fall back to its twin so the
|
|
1568
|
+
// keyboard path never dead-ends.
|
|
1569
|
+
const target =
|
|
1570
|
+
this.#findMoveButton(item, pending.direction) ??
|
|
1571
|
+
this.#findMoveButton(item, pending.direction === 1 ? -1 : 1);
|
|
1572
|
+
target?.focus({ preventScroll: true });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
#findMoveButton(item: HTMLElement, direction: -1 | 1) {
|
|
1576
|
+
const button = item.querySelector<HTMLButtonElement>(
|
|
1577
|
+
`[data-attachment-move="${direction === -1 ? "earlier" : "later"}"]`,
|
|
1578
|
+
);
|
|
1579
|
+
return button && !button.disabled ? button : null;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1354
1582
|
private _handleAttachmentKeydown(
|
|
1355
1583
|
clientId: string,
|
|
1356
1584
|
e: globalThis.KeyboardEvent,
|
|
@@ -1374,13 +1602,45 @@ export class JantComposeEditor extends LitElement {
|
|
|
1374
1602
|
}
|
|
1375
1603
|
}
|
|
1376
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* Matches when the primary pointer is a finger.
|
|
1607
|
+
*
|
|
1608
|
+
* Cached because `matchMedia` allocates a new list each call, and `undefined`
|
|
1609
|
+
* distinguishes "not looked up yet" from "this environment has no
|
|
1610
|
+
* `matchMedia`".
|
|
1611
|
+
*/
|
|
1612
|
+
#coarsePointer(): globalThis.MediaQueryList | null {
|
|
1613
|
+
if (this.#coarsePointerQuery !== undefined) return this.#coarsePointerQuery;
|
|
1614
|
+
this.#coarsePointerQuery =
|
|
1615
|
+
typeof window.matchMedia === "function"
|
|
1616
|
+
? window.matchMedia("(pointer: coarse)")
|
|
1617
|
+
: null;
|
|
1618
|
+
return this.#coarsePointerQuery;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
private _onPointerTypeChange = () => {
|
|
1622
|
+
this.#sortable?.destroy();
|
|
1623
|
+
this.#sortable = null;
|
|
1624
|
+
this.#initSortable();
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1377
1627
|
#initSortable() {
|
|
1378
1628
|
const list = this.querySelector<HTMLElement>("[data-attachment-list]");
|
|
1379
1629
|
if (!list || this.#sortable || this._attachmentOrder.length <= 1) return;
|
|
1380
1630
|
|
|
1631
|
+
// Touch reorders through the move buttons, never by dragging. This strip
|
|
1632
|
+
// scrolls along the axis it would drag, and a near-full-width thumbnail's
|
|
1633
|
+
// drop target sits off-screen — so on a touch device the gesture is worth
|
|
1634
|
+
// more as a plain scroll than as a reorder.
|
|
1635
|
+
if (this.#coarsePointer()?.matches) return;
|
|
1636
|
+
|
|
1381
1637
|
this.#sortable = Sortable.create(list, {
|
|
1382
1638
|
...responsiveSortableOptions,
|
|
1383
1639
|
chosenClass: "compose-attachment-chosen",
|
|
1640
|
+
// Only reachable on a fine-pointer device that also has a touchscreen,
|
|
1641
|
+
// where the scroll-versus-drag conflict above still applies to touch.
|
|
1642
|
+
// Longer than the shared default so an ambiguous gesture scrolls.
|
|
1643
|
+
delay: 250,
|
|
1384
1644
|
direction: "horizontal",
|
|
1385
1645
|
dragClass: "compose-attachment-drag",
|
|
1386
1646
|
filter:
|
|
@@ -1528,13 +1788,78 @@ export class JantComposeEditor extends LitElement {
|
|
|
1528
1788
|
el.style.height = `${el.scrollHeight}px`;
|
|
1529
1789
|
}
|
|
1530
1790
|
|
|
1791
|
+
/**
|
|
1792
|
+
* Where the current pointer gesture over the body went down and came up. A
|
|
1793
|
+
* `click` reports the nearest common ancestor of the two, so these are the
|
|
1794
|
+
* only way to tell a press on blank paper from a drag that merely started or
|
|
1795
|
+
* ended there.
|
|
1796
|
+
*/
|
|
1797
|
+
#bodyPressTarget: globalThis.EventTarget | null = null;
|
|
1798
|
+
#bodyReleaseTarget: globalThis.EventTarget | null = null;
|
|
1799
|
+
|
|
1800
|
+
private _recordBodyPress = (e: MouseEvent) => {
|
|
1801
|
+
this.#bodyPressTarget = e.target;
|
|
1802
|
+
this.#bodyReleaseTarget = null;
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
private _recordBodyRelease = (e: MouseEvent) => {
|
|
1806
|
+
this.#bodyReleaseTarget = e.target;
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Puts the caret in the body when the click landed on blank section instead
|
|
1811
|
+
* of on the editor. The surfaces that reserve height — the `/new` page, the
|
|
1812
|
+
* dialog on a phone — leave a stretch of empty paper under a two-line
|
|
1813
|
+
* editor, and it should behave like paper: click it, write there. Only the
|
|
1814
|
+
* layout wrappers forward; a click that reached a real control belongs to
|
|
1815
|
+
* that control.
|
|
1816
|
+
*
|
|
1817
|
+
* Selecting a line right-to-left normally lifts the pointer off the text and
|
|
1818
|
+
* over one of those wrappers, and the resulting click is retargeted to the
|
|
1819
|
+
* wrapper — indistinguishable from a click on blank paper by its target
|
|
1820
|
+
* alone. Requiring the press and the release to have landed on that same
|
|
1821
|
+
* blank space keeps a drag-selection from being thrown away.
|
|
1822
|
+
*/
|
|
1823
|
+
private _focusBodyFromBlankSpace = (e: MouseEvent) => {
|
|
1824
|
+
const pressTarget = this.#bodyPressTarget;
|
|
1825
|
+
const releaseTarget = this.#bodyReleaseTarget;
|
|
1826
|
+
this.#bodyPressTarget = null;
|
|
1827
|
+
this.#bodyReleaseTarget = null;
|
|
1828
|
+
if (pressTarget !== e.target || releaseTarget !== e.target) return;
|
|
1829
|
+
|
|
1830
|
+
const target = e.target as HTMLElement | null;
|
|
1831
|
+
if (!target) return;
|
|
1832
|
+
const forwards =
|
|
1833
|
+
target.classList.contains("compose-body") ||
|
|
1834
|
+
target.classList.contains("compose-field-enter") ||
|
|
1835
|
+
target.classList.contains("compose-tiptap-wrap");
|
|
1836
|
+
if (!forwards) return;
|
|
1837
|
+
this._editor?.commands.focus("end");
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1531
1840
|
private _setRating(star: number) {
|
|
1532
1841
|
this._rating = this._rating === star ? 0 : star;
|
|
1533
1842
|
}
|
|
1534
1843
|
|
|
1844
|
+
/**
|
|
1845
|
+
* Where a pasted or dropped image lands: inline in the body, or in the
|
|
1846
|
+
* attachment dock.
|
|
1847
|
+
*
|
|
1848
|
+
* Only a note has an article mode to fall into, and a visible, filled title
|
|
1849
|
+
* is what says the body is prose an image belongs inside. A link's body is
|
|
1850
|
+
* commentary beside a link card and a quote's is commentary beside the
|
|
1851
|
+
* quotation — images there are attachments, always. `_title` couldn't answer
|
|
1852
|
+
* this for them anyway: a link's title is required, so it is never empty
|
|
1853
|
+
* once the post is publishable, and a quote has no title field at all yet
|
|
1854
|
+
* can still be holding one carried over from a format switch.
|
|
1855
|
+
*
|
|
1856
|
+
* Only the default moves — the slash `/image` command inserts inline in
|
|
1857
|
+
* every format.
|
|
1858
|
+
*/
|
|
1535
1859
|
private _shouldPasteInlineImage(file: File): boolean {
|
|
1536
1860
|
if (!file.type.startsWith("image/")) return false;
|
|
1537
|
-
if (this.format
|
|
1861
|
+
if (this.format !== "note") return false;
|
|
1862
|
+
if (!this._showTitle) return false;
|
|
1538
1863
|
return this._title.trim().length > 0;
|
|
1539
1864
|
}
|
|
1540
1865
|
|
|
@@ -1542,7 +1867,9 @@ export class JantComposeEditor extends LitElement {
|
|
|
1542
1867
|
if (!this._fileInput) {
|
|
1543
1868
|
this._fileInput = document.createElement("input");
|
|
1544
1869
|
this._fileInput.type = "file";
|
|
1545
|
-
|
|
1870
|
+
// No `accept`: the composer takes any file, and leaving the attribute
|
|
1871
|
+
// off is how HTML says so. `*/*` is not a valid accept token, so every
|
|
1872
|
+
// browser had to work out what it meant before the dialog could open.
|
|
1546
1873
|
this._fileInput.multiple = true;
|
|
1547
1874
|
this._fileInput.style.display = "none";
|
|
1548
1875
|
this._fileInput.addEventListener("change", () =>
|
|
@@ -1588,6 +1915,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1588
1915
|
status: "pending",
|
|
1589
1916
|
progress: null,
|
|
1590
1917
|
mediaId: null,
|
|
1918
|
+
remoteUrl: null,
|
|
1591
1919
|
alt: "",
|
|
1592
1920
|
error: null,
|
|
1593
1921
|
summary: null,
|
|
@@ -1959,15 +2287,17 @@ export class JantComposeEditor extends LitElement {
|
|
|
1959
2287
|
<span class="compose-attachment-preview-fallback-label">
|
|
1960
2288
|
${unavailableLabel}
|
|
1961
2289
|
</span>
|
|
1962
|
-
${
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
<
|
|
1966
|
-
<
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2290
|
+
${
|
|
2291
|
+
category === "video"
|
|
2292
|
+
? html`
|
|
2293
|
+
<div class="compose-attachment-play-icon">
|
|
2294
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
|
2295
|
+
<path d="M8 5v14l11-7z" />
|
|
2296
|
+
</svg>
|
|
2297
|
+
</div>
|
|
2298
|
+
`
|
|
2299
|
+
: nothing
|
|
2300
|
+
}
|
|
1971
2301
|
</div>
|
|
1972
2302
|
`;
|
|
1973
2303
|
}
|
|
@@ -1977,30 +2307,19 @@ export class JantComposeEditor extends LitElement {
|
|
|
1977
2307
|
private _renderNoteFields() {
|
|
1978
2308
|
return html`
|
|
1979
2309
|
<div class="compose-field-enter">
|
|
1980
|
-
${
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
type="button"
|
|
1994
|
-
class="compose-note-title-dismiss"
|
|
1995
|
-
@click=${() => {
|
|
1996
|
-
this._showTitle = false;
|
|
1997
|
-
}}
|
|
1998
|
-
>
|
|
1999
|
-
✕
|
|
2000
|
-
</button>
|
|
2001
|
-
</div>
|
|
2002
|
-
`
|
|
2003
|
-
: nothing}
|
|
2310
|
+
${
|
|
2311
|
+
this._showTitle
|
|
2312
|
+
? html`<input
|
|
2313
|
+
type="text"
|
|
2314
|
+
.value=${this._title}
|
|
2315
|
+
@input=${(e: Event) => this._onInput("_title", e)}
|
|
2316
|
+
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2317
|
+
@keydown=${this._handleTitleKeydown}
|
|
2318
|
+
class="compose-input compose-note-title"
|
|
2319
|
+
placeholder=${this.labels.titlePlaceholder}
|
|
2320
|
+
/>`
|
|
2321
|
+
: nothing
|
|
2322
|
+
}
|
|
2004
2323
|
<div class="compose-tiptap-wrap">
|
|
2005
2324
|
<div class="compose-tiptap-body"></div>
|
|
2006
2325
|
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
@@ -2013,10 +2332,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
2013
2332
|
|
|
2014
2333
|
private _renderLinkFields() {
|
|
2015
2334
|
const urlError = this._showUrlValidation
|
|
2016
|
-
? this.
|
|
2335
|
+
? this.getUrlValidationMessage()
|
|
2017
2336
|
: null;
|
|
2018
2337
|
const titleError = this._showLinkTitleValidation
|
|
2019
|
-
? this.
|
|
2338
|
+
? this.getLinkTitleValidationMessage()
|
|
2020
2339
|
: null;
|
|
2021
2340
|
|
|
2022
2341
|
return html`
|
|
@@ -2034,7 +2353,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
2034
2353
|
@input=${(e: Event) => this._onInput("_url", e)}
|
|
2035
2354
|
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2036
2355
|
@blur=${() => {
|
|
2037
|
-
|
|
2356
|
+
// Leaving a field empty is not yet a mistake — it becomes one at
|
|
2357
|
+
// submit, where revealUrlValidation() opens this gate anyway.
|
|
2358
|
+
// Blur only speaks up about something already typed and wrong.
|
|
2359
|
+
if (this._url.trim()) this._showUrlValidation = true;
|
|
2038
2360
|
}}
|
|
2039
2361
|
class="compose-input compose-url-input"
|
|
2040
2362
|
placeholder=${this.labels.urlPlaceholder}
|
|
@@ -2044,15 +2366,17 @@ export class JantComposeEditor extends LitElement {
|
|
|
2044
2366
|
)}
|
|
2045
2367
|
/>
|
|
2046
2368
|
</div>
|
|
2047
|
-
${
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2369
|
+
${
|
|
2370
|
+
urlError
|
|
2371
|
+
? html`<p
|
|
2372
|
+
id=${this._urlStatusId}
|
|
2373
|
+
class="compose-url-status compose-url-status-error"
|
|
2374
|
+
data-compose-url-status="error"
|
|
2375
|
+
>
|
|
2376
|
+
${urlError}
|
|
2377
|
+
</p>`
|
|
2378
|
+
: nothing
|
|
2379
|
+
}
|
|
2056
2380
|
<input
|
|
2057
2381
|
type="text"
|
|
2058
2382
|
.value=${this._title}
|
|
@@ -2060,25 +2384,26 @@ export class JantComposeEditor extends LitElement {
|
|
|
2060
2384
|
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2061
2385
|
@keydown=${this._handleTitleKeydown}
|
|
2062
2386
|
@blur=${() => {
|
|
2063
|
-
|
|
2387
|
+
// Same gate as the URL above: quiet while empty, loud on submit.
|
|
2388
|
+
if (this._title.trim()) this._showLinkTitleValidation = true;
|
|
2064
2389
|
}}
|
|
2065
2390
|
class="compose-input compose-link-title"
|
|
2066
2391
|
placeholder=${this.labels.linkTitlePlaceholder}
|
|
2067
2392
|
aria-invalid=${titleError ? "true" : "false"}
|
|
2068
2393
|
/>
|
|
2069
|
-
${
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2394
|
+
${
|
|
2395
|
+
titleError
|
|
2396
|
+
? html`<p
|
|
2397
|
+
class="compose-url-status compose-url-status-error"
|
|
2398
|
+
data-compose-link-title-error="error"
|
|
2399
|
+
>
|
|
2400
|
+
${titleError}
|
|
2401
|
+
</p>`
|
|
2402
|
+
: nothing
|
|
2403
|
+
}
|
|
2077
2404
|
<div class="compose-divider"></div>
|
|
2078
2405
|
<div class="compose-tiptap-wrap">
|
|
2079
|
-
<div
|
|
2080
|
-
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-link"
|
|
2081
|
-
></div>
|
|
2406
|
+
<div class="compose-tiptap-body compose-tiptap-link"></div>
|
|
2082
2407
|
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
2083
2408
|
${this.labels.slashHint}
|
|
2084
2409
|
</span>
|
|
@@ -2102,7 +2427,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2102
2427
|
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2103
2428
|
class="compose-input compose-quote-text"
|
|
2104
2429
|
placeholder=${this.labels.quotePlaceholder}
|
|
2105
|
-
rows="
|
|
2430
|
+
rows="4"
|
|
2106
2431
|
></textarea>
|
|
2107
2432
|
</div>
|
|
2108
2433
|
<div class="compose-quote-author-row">
|
|
@@ -2132,24 +2457,24 @@ export class JantComposeEditor extends LitElement {
|
|
|
2132
2457
|
urlError ? this._urlStatusId : undefined,
|
|
2133
2458
|
)}
|
|
2134
2459
|
/>
|
|
2135
|
-
${
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2460
|
+
${
|
|
2461
|
+
urlError
|
|
2462
|
+
? html`<p
|
|
2463
|
+
id=${this._urlStatusId}
|
|
2464
|
+
class="compose-url-status compose-url-status-error"
|
|
2465
|
+
data-compose-url-status="error"
|
|
2466
|
+
>
|
|
2467
|
+
${urlError}
|
|
2468
|
+
</p>`
|
|
2469
|
+
: nothing
|
|
2470
|
+
}
|
|
2144
2471
|
</div>
|
|
2145
2472
|
<div
|
|
2146
2473
|
class="compose-divider compose-divider-quote"
|
|
2147
2474
|
aria-hidden="true"
|
|
2148
2475
|
></div>
|
|
2149
2476
|
<div class="compose-tiptap-wrap">
|
|
2150
|
-
<div
|
|
2151
|
-
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-thoughts-quote"
|
|
2152
|
-
></div>
|
|
2477
|
+
<div class="compose-tiptap-body"></div>
|
|
2153
2478
|
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
2154
2479
|
${this.labels.slashHint}
|
|
2155
2480
|
</span>
|
|
@@ -2177,9 +2502,11 @@ export class JantComposeEditor extends LitElement {
|
|
|
2177
2502
|
</button>
|
|
2178
2503
|
`,
|
|
2179
2504
|
)}
|
|
2180
|
-
${
|
|
2181
|
-
|
|
2182
|
-
|
|
2505
|
+
${
|
|
2506
|
+
this._rating > 0
|
|
2507
|
+
? html`<span class="compose-star-label">${this._rating}/5</span>`
|
|
2508
|
+
: nothing
|
|
2509
|
+
}
|
|
2183
2510
|
</div>
|
|
2184
2511
|
`;
|
|
2185
2512
|
}
|
|
@@ -2253,16 +2580,20 @@ export class JantComposeEditor extends LitElement {
|
|
|
2253
2580
|
${this._renderFileIcon(a.file.type, 20)}
|
|
2254
2581
|
</div>
|
|
2255
2582
|
<span class="compose-attachment-file-name">${a.file.name}</span>
|
|
2256
|
-
${
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2583
|
+
${
|
|
2584
|
+
a.summary
|
|
2585
|
+
? html`<span class="compose-attachment-text-summary"
|
|
2586
|
+
>${a.summary}</span
|
|
2587
|
+
>`
|
|
2588
|
+
: nothing
|
|
2589
|
+
}
|
|
2590
|
+
${
|
|
2591
|
+
typeof a.chars === "number" && a.chars > 0
|
|
2592
|
+
? html`<span class="compose-attachment-file-size"
|
|
2593
|
+
>${this._formatChars(a.chars)}</span
|
|
2594
|
+
>`
|
|
2595
|
+
: nothing
|
|
2596
|
+
}
|
|
2266
2597
|
</div>
|
|
2267
2598
|
`;
|
|
2268
2599
|
}
|
|
@@ -2292,46 +2623,52 @@ export class JantComposeEditor extends LitElement {
|
|
|
2292
2623
|
|
|
2293
2624
|
private _renderAttachmentOverlay(a: ComposeAttachment, index: number) {
|
|
2294
2625
|
return html`
|
|
2295
|
-
${
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
e
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
<
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2626
|
+
${
|
|
2627
|
+
a.status === "error"
|
|
2628
|
+
? html`
|
|
2629
|
+
<button
|
|
2630
|
+
type="button"
|
|
2631
|
+
class="compose-attachment-overlay compose-attachment-retry"
|
|
2632
|
+
@click=${(e: Event) => {
|
|
2633
|
+
e.stopPropagation();
|
|
2634
|
+
this._retryAllFailed();
|
|
2635
|
+
}}
|
|
2636
|
+
>
|
|
2637
|
+
<span class="compose-retry-content">
|
|
2638
|
+
<svg
|
|
2639
|
+
width="20"
|
|
2640
|
+
height="20"
|
|
2641
|
+
viewBox="0 0 24 24"
|
|
2642
|
+
fill="none"
|
|
2643
|
+
stroke="currentColor"
|
|
2644
|
+
stroke-width="2"
|
|
2645
|
+
stroke-linecap="round"
|
|
2646
|
+
stroke-linejoin="round"
|
|
2647
|
+
>
|
|
2648
|
+
<path
|
|
2649
|
+
d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
|
2650
|
+
/>
|
|
2651
|
+
<path d="M3 3v5h5" />
|
|
2652
|
+
<path
|
|
2653
|
+
d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"
|
|
2654
|
+
/>
|
|
2655
|
+
<path d="M16 16h5v5" />
|
|
2656
|
+
</svg>
|
|
2657
|
+
<span class="compose-retry-label"
|
|
2658
|
+
>${this.labels.retryAll}</span
|
|
2659
|
+
>
|
|
2660
|
+
</span>
|
|
2661
|
+
${
|
|
2662
|
+
a.error
|
|
2663
|
+
? html`<span class="compose-attachment-error-msg"
|
|
2664
|
+
>${a.error}</span
|
|
2665
|
+
>`
|
|
2666
|
+
: nothing
|
|
2667
|
+
}
|
|
2668
|
+
</button>
|
|
2669
|
+
`
|
|
2670
|
+
: nothing
|
|
2671
|
+
}
|
|
2335
2672
|
${this._renderAttachmentRemoveButton((e: Event) => {
|
|
2336
2673
|
e.stopPropagation();
|
|
2337
2674
|
this._removeAttachment(index);
|
|
@@ -2363,6 +2700,87 @@ export class JantComposeEditor extends LitElement {
|
|
|
2363
2700
|
`;
|
|
2364
2701
|
}
|
|
2365
2702
|
|
|
2703
|
+
private _renderAttachmentMoveButton(
|
|
2704
|
+
clientId: string,
|
|
2705
|
+
direction: -1 | 1,
|
|
2706
|
+
disabled: boolean,
|
|
2707
|
+
) {
|
|
2708
|
+
const earlier = direction === -1;
|
|
2709
|
+
const label = earlier
|
|
2710
|
+
? this.labels.moveAttachmentEarlier
|
|
2711
|
+
: this.labels.moveAttachmentLater;
|
|
2712
|
+
|
|
2713
|
+
return html`
|
|
2714
|
+
<button
|
|
2715
|
+
type="button"
|
|
2716
|
+
class="compose-attachment-move"
|
|
2717
|
+
data-attachment-move=${earlier ? "earlier" : "later"}
|
|
2718
|
+
?disabled=${disabled}
|
|
2719
|
+
aria-label=${label}
|
|
2720
|
+
title=${label}
|
|
2721
|
+
@click=${(e: Event) => {
|
|
2722
|
+
e.stopPropagation();
|
|
2723
|
+
this._moveAttachment(clientId, direction, "move");
|
|
2724
|
+
}}
|
|
2725
|
+
>
|
|
2726
|
+
<svg
|
|
2727
|
+
width="14"
|
|
2728
|
+
height="14"
|
|
2729
|
+
viewBox="0 0 14 14"
|
|
2730
|
+
fill="none"
|
|
2731
|
+
stroke="currentColor"
|
|
2732
|
+
stroke-width="1.9"
|
|
2733
|
+
stroke-linecap="round"
|
|
2734
|
+
stroke-linejoin="round"
|
|
2735
|
+
>
|
|
2736
|
+
<path
|
|
2737
|
+
d=${
|
|
2738
|
+
earlier ? "M8.75 3.5 5.25 7l3.5 3.5" : "M5.25 3.5 8.75 7l-3.5 3.5"
|
|
2739
|
+
}
|
|
2740
|
+
/>
|
|
2741
|
+
</svg>
|
|
2742
|
+
</button>
|
|
2743
|
+
`;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* Renders the quiet control row under an attachment thumbnail.
|
|
2748
|
+
*
|
|
2749
|
+
* Reorder buttons are the touch-reliable path: dragging a thumbnail competes
|
|
2750
|
+
* with the strip's own horizontal scroll, and a near-full-width thumbnail's
|
|
2751
|
+
* drop target is off-screen anyway. They appear only once there is something
|
|
2752
|
+
* to reorder against.
|
|
2753
|
+
*/
|
|
2754
|
+
private _renderAttachmentActions(
|
|
2755
|
+
clientId: string,
|
|
2756
|
+
extra: TemplateResult | typeof nothing = nothing,
|
|
2757
|
+
) {
|
|
2758
|
+
const total = this._attachmentOrder.length;
|
|
2759
|
+
const index = this._attachmentOrder.indexOf(clientId);
|
|
2760
|
+
const canReorder = total > 1 && index !== -1;
|
|
2761
|
+
if (!canReorder && extra === nothing) return nothing;
|
|
2762
|
+
|
|
2763
|
+
return html`
|
|
2764
|
+
<div class="compose-attachment-actions">
|
|
2765
|
+
${
|
|
2766
|
+
canReorder
|
|
2767
|
+
? html`
|
|
2768
|
+
<div class="compose-attachment-move-group">
|
|
2769
|
+
${this._renderAttachmentMoveButton(clientId, -1, index === 0)}
|
|
2770
|
+
${this._renderAttachmentMoveButton(
|
|
2771
|
+
clientId,
|
|
2772
|
+
1,
|
|
2773
|
+
index === total - 1,
|
|
2774
|
+
)}
|
|
2775
|
+
</div>
|
|
2776
|
+
`
|
|
2777
|
+
: nothing
|
|
2778
|
+
}
|
|
2779
|
+
${extra}
|
|
2780
|
+
</div>
|
|
2781
|
+
`;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2366
2784
|
private _renderAttachedTextCard(item: AttachedTextItem, index: number) {
|
|
2367
2785
|
return html`
|
|
2368
2786
|
<div class="compose-attachment" data-attachment-id=${item.clientId}>
|
|
@@ -2381,19 +2799,22 @@ export class JantComposeEditor extends LitElement {
|
|
|
2381
2799
|
${this._renderFileIcon("text/html; charset=utf-8", 20)}
|
|
2382
2800
|
</div>
|
|
2383
2801
|
<span class="compose-attachment-text-summary">${item.summary}</span>
|
|
2384
|
-
${
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
this.
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2802
|
+
${
|
|
2803
|
+
item.bodyJson
|
|
2804
|
+
? html`<span class="compose-attachment-file-size"
|
|
2805
|
+
>${this._formatChars(
|
|
2806
|
+
this._extractPlainText(item.bodyJson).length,
|
|
2807
|
+
)}</span
|
|
2808
|
+
>`
|
|
2809
|
+
: nothing
|
|
2810
|
+
}
|
|
2391
2811
|
</div>
|
|
2392
2812
|
${this._renderAttachmentRemoveButton((e: Event) => {
|
|
2393
2813
|
e.stopPropagation();
|
|
2394
2814
|
this._removeAttachedText(index);
|
|
2395
2815
|
})}
|
|
2396
2816
|
</div>
|
|
2817
|
+
${this._renderAttachmentActions(item.clientId)}
|
|
2397
2818
|
</div>
|
|
2398
2819
|
`;
|
|
2399
2820
|
}
|
|
@@ -2406,115 +2827,122 @@ export class JantComposeEditor extends LitElement {
|
|
|
2406
2827
|
|
|
2407
2828
|
return html`
|
|
2408
2829
|
<div class="compose-attachment" data-attachment-id=${a.clientId}>
|
|
2409
|
-
${
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
<div
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2830
|
+
${
|
|
2831
|
+
isFileCard
|
|
2832
|
+
? html`
|
|
2833
|
+
<div class="compose-attachment-thumb">
|
|
2834
|
+
<div
|
|
2835
|
+
class="compose-attachment-sortable"
|
|
2836
|
+
data-attachment-sortable
|
|
2837
|
+
tabindex="0"
|
|
2838
|
+
@keydown=${(e: globalThis.KeyboardEvent) =>
|
|
2839
|
+
this._handleAttachmentKeydown(a.clientId, e)}
|
|
2840
|
+
>
|
|
2841
|
+
${this._renderAttachmentPreview(a)}
|
|
2842
|
+
</div>
|
|
2843
|
+
${this._renderAttachmentOverlay(a, i)}
|
|
2420
2844
|
</div>
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2845
|
+
`
|
|
2846
|
+
: html`
|
|
2847
|
+
<div class="compose-attachment-thumb">
|
|
2848
|
+
<div
|
|
2849
|
+
class="compose-attachment-sortable"
|
|
2850
|
+
data-attachment-sortable
|
|
2851
|
+
tabindex="0"
|
|
2852
|
+
@keydown=${(e: globalThis.KeyboardEvent) =>
|
|
2853
|
+
this._handleAttachmentKeydown(a.clientId, e)}
|
|
2854
|
+
>
|
|
2855
|
+
${
|
|
2856
|
+
previewFailed
|
|
2857
|
+
? this._renderAttachmentPreviewFallback(visualCategory)
|
|
2858
|
+
: category === "video"
|
|
2859
|
+
? html`
|
|
2860
|
+
<video
|
|
2861
|
+
src=${a.previewUrl}
|
|
2862
|
+
poster=${a.posterUrl ?? nothing}
|
|
2863
|
+
class="compose-attachment-img"
|
|
2864
|
+
preload="metadata"
|
|
2865
|
+
.playsInline=${true}
|
|
2866
|
+
.muted=${true}
|
|
2867
|
+
@loadeddata=${() =>
|
|
2868
|
+
this._setAttachmentPreviewFailure(
|
|
2869
|
+
a.clientId,
|
|
2870
|
+
false,
|
|
2871
|
+
)}
|
|
2872
|
+
@error=${() =>
|
|
2873
|
+
this._setAttachmentPreviewFailure(
|
|
2874
|
+
a.clientId,
|
|
2875
|
+
true,
|
|
2876
|
+
)}
|
|
2877
|
+
></video>
|
|
2878
|
+
<div class="compose-attachment-play-icon">
|
|
2879
|
+
<svg
|
|
2880
|
+
width="24"
|
|
2881
|
+
height="24"
|
|
2882
|
+
viewBox="0 0 24 24"
|
|
2883
|
+
fill="white"
|
|
2884
|
+
>
|
|
2885
|
+
<path d="M8 5v14l11-7z" />
|
|
2886
|
+
</svg>
|
|
2887
|
+
</div>
|
|
2888
|
+
`
|
|
2889
|
+
: a.status === "processing"
|
|
2890
|
+
? html`
|
|
2891
|
+
<div class="compose-attachment-processing">
|
|
2892
|
+
<svg
|
|
2893
|
+
class="animate-spin size-5"
|
|
2894
|
+
viewBox="0 0 24 24"
|
|
2895
|
+
fill="none"
|
|
2896
|
+
stroke="currentColor"
|
|
2897
|
+
stroke-width="2"
|
|
2898
|
+
>
|
|
2899
|
+
<path
|
|
2900
|
+
d="M12 2a10 10 0 1 0 10 10"
|
|
2901
|
+
stroke-linecap="round"
|
|
2902
|
+
/>
|
|
2903
|
+
</svg>
|
|
2904
|
+
</div>
|
|
2905
|
+
`
|
|
2906
|
+
: html`
|
|
2907
|
+
<img
|
|
2908
|
+
src=${a.previewUrl}
|
|
2909
|
+
alt=""
|
|
2910
|
+
class="compose-attachment-img"
|
|
2911
|
+
@load=${() =>
|
|
2912
|
+
this._setAttachmentPreviewFailure(
|
|
2913
|
+
a.clientId,
|
|
2914
|
+
false,
|
|
2915
|
+
)}
|
|
2916
|
+
@error=${() =>
|
|
2917
|
+
this._setAttachmentPreviewFailure(
|
|
2918
|
+
a.clientId,
|
|
2919
|
+
true,
|
|
2920
|
+
)}
|
|
2479
2921
|
/>
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
<img
|
|
2485
|
-
src=${a.previewUrl}
|
|
2486
|
-
alt=""
|
|
2487
|
-
class="compose-attachment-img"
|
|
2488
|
-
@load=${() =>
|
|
2489
|
-
this._setAttachmentPreviewFailure(
|
|
2490
|
-
a.clientId,
|
|
2491
|
-
false,
|
|
2492
|
-
)}
|
|
2493
|
-
@error=${() =>
|
|
2494
|
-
this._setAttachmentPreviewFailure(
|
|
2495
|
-
a.clientId,
|
|
2496
|
-
true,
|
|
2497
|
-
)}
|
|
2498
|
-
/>
|
|
2499
|
-
`}
|
|
2922
|
+
`
|
|
2923
|
+
}
|
|
2924
|
+
</div>
|
|
2925
|
+
${this._renderAttachmentOverlay(a, i)}
|
|
2500
2926
|
</div>
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2927
|
+
`
|
|
2928
|
+
}
|
|
2929
|
+
${this._renderAttachmentActions(
|
|
2930
|
+
a.clientId,
|
|
2931
|
+
category === "image"
|
|
2932
|
+
? html`
|
|
2933
|
+
<button
|
|
2934
|
+
type="button"
|
|
2935
|
+
class=${classMap({
|
|
2936
|
+
"compose-attachment-alt": true,
|
|
2937
|
+
"compose-attachment-alt-set": a.alt.length > 0,
|
|
2938
|
+
})}
|
|
2939
|
+
@click=${() => this._openAltPanel(i)}
|
|
2940
|
+
>
|
|
2941
|
+
${a.alt.length > 0 ? "ALT" : "+ ALT"}
|
|
2942
|
+
</button>
|
|
2943
|
+
`
|
|
2944
|
+
: nothing,
|
|
2945
|
+
)}
|
|
2518
2946
|
</div>
|
|
2519
2947
|
`;
|
|
2520
2948
|
}
|
|
@@ -2573,11 +3001,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
2573
3001
|
@click=${() => this._openFilePicker()}
|
|
2574
3002
|
>
|
|
2575
3003
|
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.media)}
|
|
2576
|
-
${
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
3004
|
+
${
|
|
3005
|
+
this._attachments.length > 0
|
|
3006
|
+
? html`<span class="compose-tool-label"
|
|
3007
|
+
>${this.labels.addMore}</span
|
|
3008
|
+
>`
|
|
3009
|
+
: nothing
|
|
3010
|
+
}
|
|
2581
3011
|
</button>
|
|
2582
3012
|
|
|
2583
3013
|
<button
|
|
@@ -2590,11 +3020,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
2590
3020
|
@click=${() => this._openAttachedText()}
|
|
2591
3021
|
>
|
|
2592
3022
|
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.attachedText)}
|
|
2593
|
-
${
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3023
|
+
${
|
|
3024
|
+
hasAttached
|
|
3025
|
+
? html`<span class="compose-tool-label"
|
|
3026
|
+
>${this.labels.addMore}</span
|
|
3027
|
+
>`
|
|
3028
|
+
: nothing
|
|
3029
|
+
}
|
|
2598
3030
|
</button>
|
|
2599
3031
|
|
|
2600
3032
|
<button
|
|
@@ -2605,12 +3037,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
2605
3037
|
"compose-tool-btn-active": this._showEmojiPicker,
|
|
2606
3038
|
})}
|
|
2607
3039
|
title=${this.labels.emoji}
|
|
3040
|
+
aria-pressed=${this._showEmojiPicker ? "true" : "false"}
|
|
2608
3041
|
@click=${() => this._toggleEmojiPicker()}
|
|
2609
3042
|
>
|
|
2610
3043
|
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.emoji)}
|
|
2611
3044
|
</button>
|
|
2612
3045
|
|
|
2613
|
-
<div class="compose-tool-
|
|
3046
|
+
<div class="compose-tool-gap"></div>
|
|
2614
3047
|
|
|
2615
3048
|
<button
|
|
2616
3049
|
type="button"
|
|
@@ -2619,6 +3052,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2619
3052
|
"compose-tool-btn-active": this._showRating,
|
|
2620
3053
|
})}
|
|
2621
3054
|
title=${this.labels.rate}
|
|
3055
|
+
aria-pressed=${this._showRating ? "true" : "false"}
|
|
2622
3056
|
@click=${() => {
|
|
2623
3057
|
const willShow = !this._showRating;
|
|
2624
3058
|
this._showRating = willShow;
|
|
@@ -2634,46 +3068,61 @@ export class JantComposeEditor extends LitElement {
|
|
|
2634
3068
|
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.rate)}
|
|
2635
3069
|
</button>
|
|
2636
3070
|
|
|
2637
|
-
${
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
type="button"
|
|
2641
|
-
class=${classMap({
|
|
2642
|
-
"compose-tool-btn": true,
|
|
2643
|
-
"compose-tool-btn-active": this._showTitle,
|
|
2644
|
-
})}
|
|
2645
|
-
title=${this.labels.title}
|
|
2646
|
-
@click=${() => {
|
|
2647
|
-
const willShow = !this._showTitle;
|
|
2648
|
-
this._showTitle = willShow;
|
|
2649
|
-
if (willShow) {
|
|
2650
|
-
this.updateComplete.then(() => {
|
|
2651
|
-
this.querySelector<HTMLInputElement>(
|
|
2652
|
-
".compose-note-title",
|
|
2653
|
-
)?.focus();
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
}}
|
|
2657
|
-
>
|
|
2658
|
-
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.title)}
|
|
2659
|
-
</button>
|
|
2660
|
-
`
|
|
2661
|
-
: nothing}
|
|
2662
|
-
${this.format === "note"
|
|
2663
|
-
? html`
|
|
2664
|
-
<div class="compose-tool-view-group">
|
|
3071
|
+
${
|
|
3072
|
+
this.format === "note"
|
|
3073
|
+
? html`
|
|
2665
3074
|
<button
|
|
2666
3075
|
type="button"
|
|
2667
|
-
class
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3076
|
+
class=${classMap({
|
|
3077
|
+
"compose-tool-btn": true,
|
|
3078
|
+
"compose-tool-btn-active": this._showTitle,
|
|
3079
|
+
})}
|
|
3080
|
+
title=${this.labels.title}
|
|
3081
|
+
aria-pressed=${this._showTitle ? "true" : "false"}
|
|
3082
|
+
@click=${() => {
|
|
3083
|
+
const willShow = !this._showTitle;
|
|
3084
|
+
this._showTitle = willShow;
|
|
3085
|
+
// Only this deliberate click is worth remembering. The other
|
|
3086
|
+
// writers of `_showTitle` — H1 promotion, format conversion,
|
|
3087
|
+
// fullscreen reveal — are the program acting on one post, not
|
|
3088
|
+
// the author stating a habit, so they stay silent.
|
|
3089
|
+
this.dispatchEvent(
|
|
3090
|
+
new CustomEvent("jant:title-toggle", {
|
|
3091
|
+
bubbles: true,
|
|
3092
|
+
detail: { showTitle: willShow },
|
|
3093
|
+
}),
|
|
3094
|
+
);
|
|
3095
|
+
if (willShow) {
|
|
3096
|
+
this.updateComplete.then(() => {
|
|
3097
|
+
this.querySelector<HTMLInputElement>(
|
|
3098
|
+
".compose-note-title",
|
|
3099
|
+
)?.focus();
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
}}
|
|
2671
3103
|
>
|
|
2672
|
-
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.
|
|
3104
|
+
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.title)}
|
|
2673
3105
|
</button>
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
3106
|
+
`
|
|
3107
|
+
: nothing
|
|
3108
|
+
}
|
|
3109
|
+
${
|
|
3110
|
+
this.format === "note"
|
|
3111
|
+
? html`
|
|
3112
|
+
<div class="compose-tool-view-group">
|
|
3113
|
+
<button
|
|
3114
|
+
type="button"
|
|
3115
|
+
class="compose-tool-btn compose-tool-btn-view"
|
|
3116
|
+
title=${this.labels.fullscreen}
|
|
3117
|
+
aria-label=${this.labels.fullscreen}
|
|
3118
|
+
@click=${() => this.openFullscreen()}
|
|
3119
|
+
>
|
|
3120
|
+
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.fullscreen)}
|
|
3121
|
+
</button>
|
|
3122
|
+
</div>
|
|
3123
|
+
`
|
|
3124
|
+
: nothing
|
|
3125
|
+
}
|
|
2677
3126
|
</div>
|
|
2678
3127
|
`;
|
|
2679
3128
|
}
|
|
@@ -2689,14 +3138,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
2689
3138
|
|
|
2690
3139
|
return html`
|
|
2691
3140
|
<div class="compose-thread-post-header">
|
|
2692
|
-
<div class="compose-segmented
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
"compose-format-pill-link": this.format === "link",
|
|
2697
|
-
"compose-format-pill-quote": this.format === "quote",
|
|
2698
|
-
})}
|
|
2699
|
-
></div>
|
|
3141
|
+
<div class="compose-segmented">
|
|
3142
|
+
<!-- No sliding indicator: the items size to their labels now, so a
|
|
3143
|
+
thirds-based pill would not line up. The active item carries its
|
|
3144
|
+
own background instead. -->
|
|
2700
3145
|
${formats.map(
|
|
2701
3146
|
(f) => html`
|
|
2702
3147
|
<button
|
|
@@ -2721,31 +3166,48 @@ export class JantComposeEditor extends LitElement {
|
|
|
2721
3166
|
`,
|
|
2722
3167
|
)}
|
|
2723
3168
|
</div>
|
|
2724
|
-
${
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3169
|
+
${
|
|
3170
|
+
this.positionLabel
|
|
3171
|
+
? html`<span class="compose-post-position"
|
|
3172
|
+
>${this.positionLabel}</span
|
|
3173
|
+
>`
|
|
3174
|
+
: nothing
|
|
3175
|
+
}
|
|
3176
|
+
${
|
|
3177
|
+
this.badgeLabel
|
|
3178
|
+
? html`<span class="compose-post-position compose-post-badge"
|
|
3179
|
+
>${this.badgeLabel}</span
|
|
3180
|
+
>`
|
|
3181
|
+
: nothing
|
|
3182
|
+
}
|
|
3183
|
+
${this.headerExtra ?? nothing}
|
|
3184
|
+
${
|
|
3185
|
+
this.removable
|
|
3186
|
+
? html`<button
|
|
3187
|
+
type="button"
|
|
3188
|
+
class="compose-thread-post-remove"
|
|
3189
|
+
title="Remove post"
|
|
3190
|
+
@click=${() => {
|
|
3191
|
+
this.dispatchEvent(
|
|
3192
|
+
new CustomEvent("jant:thread-remove", { bubbles: true }),
|
|
3193
|
+
);
|
|
3194
|
+
}}
|
|
2744
3195
|
>
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3196
|
+
<svg
|
|
3197
|
+
width="14"
|
|
3198
|
+
height="14"
|
|
3199
|
+
viewBox="0 0 16 16"
|
|
3200
|
+
fill="none"
|
|
3201
|
+
stroke="currentColor"
|
|
3202
|
+
stroke-width="1.8"
|
|
3203
|
+
stroke-linecap="round"
|
|
3204
|
+
aria-hidden="true"
|
|
3205
|
+
>
|
|
3206
|
+
${unsafeSVG(CLOSE_ICON)}
|
|
3207
|
+
</svg>
|
|
3208
|
+
</button>`
|
|
3209
|
+
: nothing
|
|
3210
|
+
}
|
|
2749
3211
|
</div>
|
|
2750
3212
|
`;
|
|
2751
3213
|
}
|
|
@@ -2762,16 +3224,23 @@ export class JantComposeEditor extends LitElement {
|
|
|
2762
3224
|
}
|
|
2763
3225
|
|
|
2764
3226
|
render() {
|
|
3227
|
+
// The format selector lives here in every mode — single post, reply, and
|
|
3228
|
+
// thread item alike — so there is exactly one place it can be.
|
|
2765
3229
|
return html`
|
|
2766
|
-
${this.
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3230
|
+
${this._renderFormatHeader()}
|
|
3231
|
+
<section
|
|
3232
|
+
class="compose-body"
|
|
3233
|
+
@mousedown=${this._recordBodyPress}
|
|
3234
|
+
@mouseup=${this._recordBodyRelease}
|
|
3235
|
+
@click=${this._focusBodyFromBlankSpace}
|
|
3236
|
+
>
|
|
3237
|
+
${
|
|
3238
|
+
this.format === "note"
|
|
3239
|
+
? this._renderNoteFields()
|
|
3240
|
+
: this.format === "link"
|
|
3241
|
+
? this._renderLinkFields()
|
|
3242
|
+
: this._renderQuoteFields()
|
|
3243
|
+
}
|
|
2775
3244
|
</section>
|
|
2776
3245
|
${this._renderAttachmentDock()} ${this._renderStarRating()}
|
|
2777
3246
|
${this._renderToolsRow()}
|