@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
|
@@ -7,11 +7,12 @@ vi.mock("../../upload-with-metadata.js", () => ({
|
|
|
7
7
|
import type { Editor, JSONContent } from "@tiptap/core";
|
|
8
8
|
import type { Slice } from "@tiptap/pm/model";
|
|
9
9
|
import * as ProseMirrorView from "@tiptap/pm/view";
|
|
10
|
-
import type { ComposeLabels } from "../compose-types.js";
|
|
10
|
+
import type { ComposeAttachment, ComposeLabels } from "../compose-types.js";
|
|
11
11
|
import { renderTiptapJson } from "../../../lib/tiptap-render.js";
|
|
12
12
|
import "../jant-compose-editor.js";
|
|
13
13
|
import type { JantComposeEditor } from "../jant-compose-editor.js";
|
|
14
14
|
import { uploadWithMetadata } from "../../upload-with-metadata.js";
|
|
15
|
+
import Sortable from "sortablejs";
|
|
15
16
|
|
|
16
17
|
function requireElement<T extends globalThis.Element>(
|
|
17
18
|
element: T | null,
|
|
@@ -108,6 +109,57 @@ function triggerEditorPaste(el: JantComposeEditor, files: File[]) {
|
|
|
108
109
|
return { handled, event };
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Drops files on the element itself, the way a drag onto the composer chrome
|
|
114
|
+
* (rather than into the ProseMirror body) arrives.
|
|
115
|
+
*/
|
|
116
|
+
function triggerEditorDrop(el: JantComposeEditor, files: File[]) {
|
|
117
|
+
const event = new Event("drop", { bubbles: true, cancelable: true });
|
|
118
|
+
Object.defineProperty(event, "dataTransfer", {
|
|
119
|
+
value: {
|
|
120
|
+
items: files.map((file) => ({
|
|
121
|
+
kind: "file",
|
|
122
|
+
type: file.type,
|
|
123
|
+
getAsFile: () => file,
|
|
124
|
+
})),
|
|
125
|
+
files,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
el.dispatchEvent(event);
|
|
129
|
+
return event;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Every image src currently in the editor's document, in document order. */
|
|
133
|
+
function imageSrcs(el: JantComposeEditor): string[] {
|
|
134
|
+
const srcs: string[] = [];
|
|
135
|
+
requireEditor(el).state.doc.descendants((node) => {
|
|
136
|
+
if (node.type.name === "image") srcs.push(node.attrs.src as string);
|
|
137
|
+
});
|
|
138
|
+
return srcs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The same, read off the JSON the composer would submit. */
|
|
142
|
+
function bodyImageSrcs(json: unknown): string[] {
|
|
143
|
+
const srcs: string[] = [];
|
|
144
|
+
const walk = (node: unknown) => {
|
|
145
|
+
if (!node || typeof node !== "object") return;
|
|
146
|
+
const candidate = node as {
|
|
147
|
+
type?: string;
|
|
148
|
+
attrs?: { src?: unknown };
|
|
149
|
+
content?: unknown[];
|
|
150
|
+
};
|
|
151
|
+
if (
|
|
152
|
+
candidate.type === "image" &&
|
|
153
|
+
typeof candidate.attrs?.src === "string"
|
|
154
|
+
) {
|
|
155
|
+
srcs.push(candidate.attrs.src);
|
|
156
|
+
}
|
|
157
|
+
for (const child of candidate.content ?? []) walk(child);
|
|
158
|
+
};
|
|
159
|
+
walk(json);
|
|
160
|
+
return srcs;
|
|
161
|
+
}
|
|
162
|
+
|
|
111
163
|
function parsePastedText(el: JantComposeEditor, text: string): Slice | null {
|
|
112
164
|
const editor = requireEditor(el);
|
|
113
165
|
let slice: Slice | null = null;
|
|
@@ -152,6 +204,7 @@ const parseFromClipboard = (
|
|
|
152
204
|
|
|
153
205
|
const labels: ComposeLabels = {
|
|
154
206
|
cancel: "Cancel",
|
|
207
|
+
close: "Close",
|
|
155
208
|
note: "Note",
|
|
156
209
|
link: "Link",
|
|
157
210
|
quote: "Quote",
|
|
@@ -190,7 +243,11 @@ const labels: ComposeLabels = {
|
|
|
190
243
|
altHint: "Alt text improves accessibility",
|
|
191
244
|
addMore: "Add",
|
|
192
245
|
removeAttachment: "Remove attachment",
|
|
246
|
+
moveAttachmentEarlier: "Move attachment earlier",
|
|
247
|
+
moveAttachmentLater: "Move attachment later",
|
|
193
248
|
uploading: "Uploading...",
|
|
249
|
+
publishing: "Publishing...",
|
|
250
|
+
saving: "Saving...",
|
|
194
251
|
loadingPost: "Loading post...",
|
|
195
252
|
loadPostFailed: "Couldn't load this post. Try again.",
|
|
196
253
|
published: "Published!",
|
|
@@ -201,7 +258,7 @@ const labels: ComposeLabels = {
|
|
|
201
258
|
confirmCloseTitle: "Save to drafts?",
|
|
202
259
|
confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
|
|
203
260
|
confirmCloseSave: "Save",
|
|
204
|
-
confirmCloseCancel: "
|
|
261
|
+
confirmCloseCancel: "Keep editing",
|
|
205
262
|
confirmCloseDiscard: "Don't save",
|
|
206
263
|
confirmAttachedTitle: "Save text attachment?",
|
|
207
264
|
confirmAttachedSubtitle:
|
|
@@ -220,14 +277,32 @@ const labels: ComposeLabels = {
|
|
|
220
277
|
deleteDraft: "Delete Draft",
|
|
221
278
|
draftDeleted: "Draft deleted.",
|
|
222
279
|
publishFailedDraft: "Couldn't publish. Saved as draft.",
|
|
280
|
+
publishFailed: "Couldn't publish. Try again.",
|
|
281
|
+
publishFailedOffline: "Couldn't reach the server. Try again.",
|
|
223
282
|
uploadFailedDraft: "Some uploads failed. Saved as draft.",
|
|
224
283
|
addCollection: "Add Collection",
|
|
225
284
|
collectionCountLabel: "%name% + %count% more",
|
|
226
285
|
draftRestored: "Draft restored.",
|
|
286
|
+
draftStoreFailed: "This browser can't keep a local copy. Save as a draft.",
|
|
227
287
|
reply: "Reply",
|
|
228
288
|
publishHideFromLatest: "Hide from Latest",
|
|
229
289
|
publishPrivate: "Post as Private",
|
|
230
290
|
publishSettings: "Publish settings",
|
|
291
|
+
languageLabel: "Language",
|
|
292
|
+
languageAuto: "Detect",
|
|
293
|
+
languageAutoHint: "Read from what you write",
|
|
294
|
+
languageAutoDetected: "Read from what you write — looks like {language}",
|
|
295
|
+
languageAutoPending: "Not enough text to tell yet — publishes in {language}",
|
|
296
|
+
languageTriggerLabel: "Language: {language}",
|
|
297
|
+
translationOf: "Translation of “{title}”",
|
|
298
|
+
translationContext: "Translating",
|
|
299
|
+
translationContextInLanguage: "Translating into {language}",
|
|
300
|
+
translationContextOpen: "Open the original in a new tab",
|
|
301
|
+
translationContextOriginal: "The original",
|
|
302
|
+
translationContextHide: "Hide",
|
|
303
|
+
translationContextHideLong: "Hide the original",
|
|
304
|
+
translationContextShow: "Show",
|
|
305
|
+
translationContextShowLong: "Show the original",
|
|
231
306
|
publishVisibilityLabel: "Visibility",
|
|
232
307
|
publishVisibilityPublic: "Public",
|
|
233
308
|
publishVisibilityPublicHint: "Appears in Latest.",
|
|
@@ -243,13 +318,13 @@ const labels: ComposeLabels = {
|
|
|
243
318
|
publishDateInvalid: "Enter a valid date.",
|
|
244
319
|
publishDateFutureError:
|
|
245
320
|
"Choose today or an earlier date, or leave it blank to publish now.",
|
|
246
|
-
publishDateSummaryNow: "
|
|
321
|
+
publishDateSummaryNow: "Now",
|
|
247
322
|
publishDateSummaryAction: "Edit publish date",
|
|
248
323
|
publishSlugLabel: "Custom link",
|
|
249
324
|
publishSlugPlaceholder: "your-post-link",
|
|
250
325
|
publishSlugHint: "Leave blank to generate one automatically.",
|
|
251
326
|
publishSlugAuto: "Generate automatically",
|
|
252
|
-
publishSlugSummaryAuto: "Auto
|
|
327
|
+
publishSlugSummaryAuto: "Auto",
|
|
253
328
|
publishSlugSummaryAction: "Edit custom link",
|
|
254
329
|
publishSlugReset: "Reset link",
|
|
255
330
|
publishSlugSuggested: "Suggested link",
|
|
@@ -265,9 +340,8 @@ const labels: ComposeLabels = {
|
|
|
265
340
|
threadLimitReached: "Threads can include up to 20 posts.",
|
|
266
341
|
showMore: "Show more",
|
|
267
342
|
showLess: "Show less",
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
editTitle: "Edit",
|
|
343
|
+
editing: "Editing",
|
|
344
|
+
composeDialogLabel: "Compose",
|
|
271
345
|
slashHint: "Type / for commands",
|
|
272
346
|
tableControls: {
|
|
273
347
|
toolbarLabel: "Table controls",
|
|
@@ -295,28 +369,77 @@ const labels: ComposeLabels = {
|
|
|
295
369
|
quickHint: "More options are available after you create it.",
|
|
296
370
|
quickSubmitLabel: "Done",
|
|
297
371
|
createdLabel: "Collection created.",
|
|
298
|
-
descriptionLabel: "Description (optional)",
|
|
299
|
-
descriptionPlaceholder: "What's this collection about?",
|
|
300
|
-
sortOrderLabel: "Sort Order",
|
|
301
|
-
sortNewest: "Newest first",
|
|
302
|
-
sortOldest: "Oldest first",
|
|
303
|
-
sortRatingDesc: "Highest rated",
|
|
304
|
-
submitLabel: "Save",
|
|
305
372
|
cancelLabel: "Cancel",
|
|
306
373
|
},
|
|
307
374
|
};
|
|
308
375
|
|
|
309
376
|
async function createElement(
|
|
310
377
|
format: string = "note",
|
|
378
|
+
options: { titleByDefault?: boolean } = {},
|
|
311
379
|
): Promise<JantComposeEditor> {
|
|
312
380
|
const el = document.createElement("jant-compose-editor") as JantComposeEditor;
|
|
313
381
|
el.format = format as "note" | "link" | "quote";
|
|
314
382
|
el.labels = labels;
|
|
383
|
+
if (options.titleByDefault !== undefined) {
|
|
384
|
+
el.titleByDefault = options.titleByDefault;
|
|
385
|
+
}
|
|
315
386
|
document.body.appendChild(el);
|
|
316
387
|
await el.updateComplete;
|
|
317
388
|
return el;
|
|
318
389
|
}
|
|
319
390
|
|
|
391
|
+
function mediaAttachment(clientId: string, mediaId: string): ComposeAttachment {
|
|
392
|
+
const blob = new Blob(["fake"], { type: "image/png" });
|
|
393
|
+
return {
|
|
394
|
+
clientId,
|
|
395
|
+
file: new File([blob], `${clientId}.png`, { type: "image/png" }),
|
|
396
|
+
previewUrl: URL.createObjectURL(blob),
|
|
397
|
+
status: "done",
|
|
398
|
+
progress: null,
|
|
399
|
+
mediaId,
|
|
400
|
+
alt: "",
|
|
401
|
+
error: null,
|
|
402
|
+
posterUrl: null,
|
|
403
|
+
remoteUrl: null,
|
|
404
|
+
summary: null,
|
|
405
|
+
chars: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Forces the primary-pointer media query, and returns a restore function.
|
|
411
|
+
* Every call site restores in a `finally` so one test cannot leak into another.
|
|
412
|
+
*/
|
|
413
|
+
function stubPrimaryPointer(kind: "coarse" | "fine"): () => void {
|
|
414
|
+
const original = window.matchMedia;
|
|
415
|
+
// Overrides `matches` on a real MediaQueryList, so the component still gets
|
|
416
|
+
// a live object to subscribe to.
|
|
417
|
+
window.matchMedia = (query: string) => {
|
|
418
|
+
const list = original.call(window, query);
|
|
419
|
+
Object.defineProperty(list, "matches", {
|
|
420
|
+
configurable: true,
|
|
421
|
+
value: query.includes(`pointer: ${kind}`),
|
|
422
|
+
});
|
|
423
|
+
return list;
|
|
424
|
+
};
|
|
425
|
+
return () => {
|
|
426
|
+
window.matchMedia = original;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function moveButton(
|
|
431
|
+
el: JantComposeEditor,
|
|
432
|
+
clientId: string,
|
|
433
|
+
direction: "earlier" | "later",
|
|
434
|
+
): HTMLButtonElement {
|
|
435
|
+
return requireElement(
|
|
436
|
+
el.querySelector<HTMLButtonElement>(
|
|
437
|
+
`[data-attachment-id="${clientId}"] [data-attachment-move="${direction}"]`,
|
|
438
|
+
),
|
|
439
|
+
`expected ${direction} move button for ${clientId}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
320
443
|
function tiptapDoc(...content: JSONContent[]): JSONContent {
|
|
321
444
|
return { type: "doc", content };
|
|
322
445
|
}
|
|
@@ -336,6 +459,22 @@ function tiptapParagraph(text: string): JSONContent {
|
|
|
336
459
|
};
|
|
337
460
|
}
|
|
338
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Replays a pointer gesture. The browser retargets a click to the nearest
|
|
464
|
+
* common ancestor of the press and the release, so `clickTarget` is separate
|
|
465
|
+
* from the two — a drag that starts on the text and ends over a layout wrapper
|
|
466
|
+
* reports the wrapper.
|
|
467
|
+
*/
|
|
468
|
+
function pressAndClick(
|
|
469
|
+
pressTarget: HTMLElement,
|
|
470
|
+
releaseTarget: HTMLElement,
|
|
471
|
+
clickTarget: HTMLElement = releaseTarget,
|
|
472
|
+
) {
|
|
473
|
+
pressTarget.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
474
|
+
releaseTarget.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
475
|
+
clickTarget.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
476
|
+
}
|
|
477
|
+
|
|
339
478
|
async function toggleEmojiPicker(el: JantComposeEditor) {
|
|
340
479
|
(
|
|
341
480
|
el as unknown as {
|
|
@@ -795,13 +934,46 @@ describe("JantComposeEditor", () => {
|
|
|
795
934
|
expect(el._attachedTexts[0].bodyJson).toBeNull();
|
|
796
935
|
});
|
|
797
936
|
|
|
798
|
-
it("shows title
|
|
937
|
+
it("shows a note's title field by default on a post that starts a thread", async () => {
|
|
799
938
|
const el = await createElement("note");
|
|
939
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
800
940
|
expect(el.querySelector('.compose-tool-btn[title="Title"]')).not.toBeNull();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it("hides a note's title field by default on a continuation post", async () => {
|
|
944
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
945
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it("toggles the title field from the toolbar", async () => {
|
|
949
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
950
|
+
const toggle = requireElement(
|
|
951
|
+
el.querySelector<HTMLButtonElement>('.compose-tool-btn[title="Title"]'),
|
|
952
|
+
"expected title toggle",
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
toggle.click();
|
|
956
|
+
await el.updateComplete;
|
|
957
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
958
|
+
|
|
959
|
+
toggle.click();
|
|
960
|
+
await el.updateComplete;
|
|
961
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
962
|
+
});
|
|
801
963
|
|
|
802
|
-
|
|
964
|
+
it("drops a hidden title from the submitted data", async () => {
|
|
965
|
+
const el = await createElement("note");
|
|
966
|
+
el._title = "Named";
|
|
967
|
+
el._showTitle = false;
|
|
803
968
|
await el.updateComplete;
|
|
804
|
-
expect(el.
|
|
969
|
+
expect(el.getData().title).toBe("");
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it("keeps the title field visible when a post arrives with one", async () => {
|
|
973
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
974
|
+
el.populate({ format: "note", title: "Loaded title" });
|
|
975
|
+
await el.updateComplete;
|
|
976
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
805
977
|
});
|
|
806
978
|
|
|
807
979
|
it.each([
|
|
@@ -812,7 +984,6 @@ describe("JantComposeEditor", () => {
|
|
|
812
984
|
async (format, selector) => {
|
|
813
985
|
const el = await createElement(format);
|
|
814
986
|
if (format === "note") {
|
|
815
|
-
el._showTitle = true;
|
|
816
987
|
await el.updateComplete;
|
|
817
988
|
}
|
|
818
989
|
|
|
@@ -841,7 +1012,6 @@ describe("JantComposeEditor", () => {
|
|
|
841
1012
|
|
|
842
1013
|
it("keeps focus in the title while an IME composition is active", async () => {
|
|
843
1014
|
const el = await createElement("note");
|
|
844
|
-
el._showTitle = true;
|
|
845
1015
|
await el.updateComplete;
|
|
846
1016
|
|
|
847
1017
|
const titleInput = requireElement(
|
|
@@ -864,7 +1034,6 @@ describe("JantComposeEditor", () => {
|
|
|
864
1034
|
|
|
865
1035
|
it("leaves Mod-Enter available for the publish shortcut", async () => {
|
|
866
1036
|
const el = await createElement("note");
|
|
867
|
-
el._showTitle = true;
|
|
868
1037
|
await el.updateComplete;
|
|
869
1038
|
|
|
870
1039
|
const titleInput = requireElement(
|
|
@@ -885,7 +1054,7 @@ describe("JantComposeEditor", () => {
|
|
|
885
1054
|
expect(document.activeElement).toBe(titleInput);
|
|
886
1055
|
});
|
|
887
1056
|
|
|
888
|
-
it("
|
|
1057
|
+
it("places fullscreen at the far right of the toolbar", async () => {
|
|
889
1058
|
const el = await createElement("note");
|
|
890
1059
|
const toolTitles = [
|
|
891
1060
|
...el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn"),
|
|
@@ -924,7 +1093,6 @@ describe("JantComposeEditor", () => {
|
|
|
924
1093
|
it("getData returns current field values", async () => {
|
|
925
1094
|
const el = await createElement("note");
|
|
926
1095
|
el._title = "Test Title";
|
|
927
|
-
el._showTitle = true;
|
|
928
1096
|
el._bodyJson = {
|
|
929
1097
|
type: "doc",
|
|
930
1098
|
content: [
|
|
@@ -994,31 +1162,11 @@ describe("JantComposeEditor", () => {
|
|
|
994
1162
|
expect(data.attachmentOrder).toEqual(["t2"]);
|
|
995
1163
|
});
|
|
996
1164
|
|
|
997
|
-
it("getData
|
|
1165
|
+
it("getData returns the note title as typed", async () => {
|
|
998
1166
|
const el = await createElement("note");
|
|
999
|
-
el._title = "
|
|
1000
|
-
el._showTitle = false;
|
|
1001
|
-
|
|
1002
|
-
const data = el.getData();
|
|
1003
|
-
expect(data.title).toBe("");
|
|
1004
|
-
});
|
|
1167
|
+
el._title = "Kept Title";
|
|
1005
1168
|
|
|
1006
|
-
|
|
1007
|
-
const el = await createElement("note");
|
|
1008
|
-
el._title = "My Title";
|
|
1009
|
-
el._showTitle = true;
|
|
1010
|
-
await el.updateComplete;
|
|
1011
|
-
|
|
1012
|
-
// Toggle off — title stays in memory
|
|
1013
|
-
el._showTitle = false;
|
|
1014
|
-
await el.updateComplete;
|
|
1015
|
-
expect(el._title).toBe("My Title");
|
|
1016
|
-
expect(el.getData().title).toBe("");
|
|
1017
|
-
|
|
1018
|
-
// Toggle back on — title restored
|
|
1019
|
-
el._showTitle = true;
|
|
1020
|
-
await el.updateComplete;
|
|
1021
|
-
expect(el.getData().title).toBe("My Title");
|
|
1169
|
+
expect(el.getData().title).toBe("Kept Title");
|
|
1022
1170
|
});
|
|
1023
1171
|
|
|
1024
1172
|
it("promotes a leading H1 to the note title", async () => {
|
|
@@ -1035,7 +1183,6 @@ describe("JantComposeEditor", () => {
|
|
|
1035
1183
|
const body = JSON.parse(data.body) as JSONContent;
|
|
1036
1184
|
|
|
1037
1185
|
expect(data.title).toBe("My Markdown Title");
|
|
1038
|
-
expect(el._showTitle).toBe(true);
|
|
1039
1186
|
expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
|
|
1040
1187
|
});
|
|
1041
1188
|
|
|
@@ -1050,7 +1197,6 @@ describe("JantComposeEditor", () => {
|
|
|
1050
1197
|
const body = JSON.parse(data.body) as JSONContent;
|
|
1051
1198
|
|
|
1052
1199
|
expect(data.title).toBe("");
|
|
1053
|
-
expect(el._showTitle).toBe(false);
|
|
1054
1200
|
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
|
|
1055
1201
|
});
|
|
1056
1202
|
|
|
@@ -1058,7 +1204,6 @@ describe("JantComposeEditor", () => {
|
|
|
1058
1204
|
const el = await createElement("note");
|
|
1059
1205
|
const editor = requireEditor(el);
|
|
1060
1206
|
el._title = "Manual title";
|
|
1061
|
-
el._showTitle = true;
|
|
1062
1207
|
|
|
1063
1208
|
editor.commands.setContent(
|
|
1064
1209
|
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
@@ -1231,6 +1376,7 @@ describe("JantComposeEditor", () => {
|
|
|
1231
1376
|
alt: "",
|
|
1232
1377
|
error: null,
|
|
1233
1378
|
posterUrl: null,
|
|
1379
|
+
remoteUrl: null,
|
|
1234
1380
|
summary: null,
|
|
1235
1381
|
chars: null,
|
|
1236
1382
|
},
|
|
@@ -1251,35 +1397,9 @@ describe("JantComposeEditor", () => {
|
|
|
1251
1397
|
|
|
1252
1398
|
it("moves attachments later with keyboard controls", async () => {
|
|
1253
1399
|
const el = await createElement("note");
|
|
1254
|
-
const blob = new Blob(["fake"], { type: "image/png" });
|
|
1255
|
-
const file = new File([blob], "test.png", { type: "image/png" });
|
|
1256
1400
|
el._attachments = [
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
file,
|
|
1260
|
-
previewUrl: URL.createObjectURL(blob),
|
|
1261
|
-
status: "done",
|
|
1262
|
-
progress: null,
|
|
1263
|
-
mediaId: "m1",
|
|
1264
|
-
alt: "",
|
|
1265
|
-
error: null,
|
|
1266
|
-
posterUrl: null,
|
|
1267
|
-
summary: null,
|
|
1268
|
-
chars: null,
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
1271
|
-
clientId: "a2",
|
|
1272
|
-
file,
|
|
1273
|
-
previewUrl: URL.createObjectURL(blob),
|
|
1274
|
-
status: "done",
|
|
1275
|
-
progress: null,
|
|
1276
|
-
mediaId: "m2",
|
|
1277
|
-
alt: "",
|
|
1278
|
-
error: null,
|
|
1279
|
-
posterUrl: null,
|
|
1280
|
-
summary: null,
|
|
1281
|
-
chars: null,
|
|
1282
|
-
},
|
|
1401
|
+
mediaAttachment("a1", "m1"),
|
|
1402
|
+
mediaAttachment("a2", "m2"),
|
|
1283
1403
|
];
|
|
1284
1404
|
el._attachmentOrder = ["a1", "a2"];
|
|
1285
1405
|
await el.updateComplete;
|
|
@@ -1301,6 +1421,127 @@ describe("JantComposeEditor", () => {
|
|
|
1301
1421
|
expect(el._attachmentOrder).toEqual(["a2", "a1"]);
|
|
1302
1422
|
});
|
|
1303
1423
|
|
|
1424
|
+
it("reorders attachments with the move buttons", async () => {
|
|
1425
|
+
const el = await createElement("note");
|
|
1426
|
+
el._attachments = [
|
|
1427
|
+
mediaAttachment("a1", "m1"),
|
|
1428
|
+
mediaAttachment("a2", "m2"),
|
|
1429
|
+
mediaAttachment("a3", "m3"),
|
|
1430
|
+
];
|
|
1431
|
+
el._attachmentOrder = ["a1", "a2", "a3"];
|
|
1432
|
+
await el.updateComplete;
|
|
1433
|
+
|
|
1434
|
+
moveButton(el, "a1", "later").click();
|
|
1435
|
+
await el.updateComplete;
|
|
1436
|
+
expect(el._attachmentOrder).toEqual(["a2", "a1", "a3"]);
|
|
1437
|
+
|
|
1438
|
+
moveButton(el, "a1", "earlier").click();
|
|
1439
|
+
await el.updateComplete;
|
|
1440
|
+
expect(el._attachmentOrder).toEqual(["a1", "a2", "a3"]);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it("disables the move button at each end of the strip", async () => {
|
|
1444
|
+
const el = await createElement("note");
|
|
1445
|
+
el._attachments = [
|
|
1446
|
+
mediaAttachment("a1", "m1"),
|
|
1447
|
+
mediaAttachment("a2", "m2"),
|
|
1448
|
+
];
|
|
1449
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1450
|
+
await el.updateComplete;
|
|
1451
|
+
|
|
1452
|
+
expect(moveButton(el, "a1", "earlier").disabled).toBe(true);
|
|
1453
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(false);
|
|
1454
|
+
expect(moveButton(el, "a2", "earlier").disabled).toBe(false);
|
|
1455
|
+
expect(moveButton(el, "a2", "later").disabled).toBe(true);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it("keeps focus on the attachment that moved", async () => {
|
|
1459
|
+
const el = await createElement("note");
|
|
1460
|
+
el._attachments = [
|
|
1461
|
+
mediaAttachment("a1", "m1"),
|
|
1462
|
+
mediaAttachment("a2", "m2"),
|
|
1463
|
+
mediaAttachment("a3", "m3"),
|
|
1464
|
+
];
|
|
1465
|
+
el._attachmentOrder = ["a1", "a2", "a3"];
|
|
1466
|
+
await el.updateComplete;
|
|
1467
|
+
|
|
1468
|
+
const later = moveButton(el, "a1", "later");
|
|
1469
|
+
later.focus();
|
|
1470
|
+
later.click();
|
|
1471
|
+
await el.updateComplete;
|
|
1472
|
+
|
|
1473
|
+
expect(document.activeElement).toBe(moveButton(el, "a1", "later"));
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("falls back to the opposite button when a move reaches the end", async () => {
|
|
1477
|
+
const el = await createElement("note");
|
|
1478
|
+
el._attachments = [
|
|
1479
|
+
mediaAttachment("a1", "m1"),
|
|
1480
|
+
mediaAttachment("a2", "m2"),
|
|
1481
|
+
];
|
|
1482
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1483
|
+
await el.updateComplete;
|
|
1484
|
+
|
|
1485
|
+
const later = moveButton(el, "a1", "later");
|
|
1486
|
+
later.focus();
|
|
1487
|
+
later.click();
|
|
1488
|
+
await el.updateComplete;
|
|
1489
|
+
|
|
1490
|
+
expect(el._attachmentOrder).toEqual(["a2", "a1"]);
|
|
1491
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(true);
|
|
1492
|
+
expect(document.activeElement).toBe(moveButton(el, "a1", "earlier"));
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
it("omits move buttons when there is nothing to reorder", async () => {
|
|
1496
|
+
const el = await createElement("note");
|
|
1497
|
+
el._attachments = [mediaAttachment("a1", "m1")];
|
|
1498
|
+
el._attachmentOrder = ["a1"];
|
|
1499
|
+
await el.updateComplete;
|
|
1500
|
+
|
|
1501
|
+
expect(el.querySelector("[data-attachment-move]")).toBeNull();
|
|
1502
|
+
expect(el.querySelector(".compose-attachment-alt")).not.toBeNull();
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it("leaves drag reordering off on a touch device", async () => {
|
|
1506
|
+
const create = vi.spyOn(Sortable, "create");
|
|
1507
|
+
const restorePointer = stubPrimaryPointer("coarse");
|
|
1508
|
+
try {
|
|
1509
|
+
const el = await createElement("note");
|
|
1510
|
+
el._attachments = [
|
|
1511
|
+
mediaAttachment("a1", "m1"),
|
|
1512
|
+
mediaAttachment("a2", "m2"),
|
|
1513
|
+
];
|
|
1514
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1515
|
+
await el.updateComplete;
|
|
1516
|
+
|
|
1517
|
+
expect(create).not.toHaveBeenCalled();
|
|
1518
|
+
// The buttons are the whole reordering path there, so they must stay.
|
|
1519
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(false);
|
|
1520
|
+
} finally {
|
|
1521
|
+
restorePointer();
|
|
1522
|
+
create.mockRestore();
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("keeps drag reordering on a pointer device", async () => {
|
|
1527
|
+
const create = vi.spyOn(Sortable, "create");
|
|
1528
|
+
const restorePointer = stubPrimaryPointer("fine");
|
|
1529
|
+
try {
|
|
1530
|
+
const el = await createElement("note");
|
|
1531
|
+
el._attachments = [
|
|
1532
|
+
mediaAttachment("a1", "m1"),
|
|
1533
|
+
mediaAttachment("a2", "m2"),
|
|
1534
|
+
];
|
|
1535
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1536
|
+
await el.updateComplete;
|
|
1537
|
+
|
|
1538
|
+
expect(create).toHaveBeenCalledTimes(1);
|
|
1539
|
+
} finally {
|
|
1540
|
+
restorePointer();
|
|
1541
|
+
create.mockRestore();
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1304
1545
|
it("preserves mixed attachment order when populate provides one", async () => {
|
|
1305
1546
|
const el = await createElement("note");
|
|
1306
1547
|
|
|
@@ -1343,7 +1584,7 @@ describe("JantComposeEditor", () => {
|
|
|
1343
1584
|
expect(el._attachmentOrder[0]).toBe("t1");
|
|
1344
1585
|
});
|
|
1345
1586
|
|
|
1346
|
-
it("pastes clipboard files into attachments when no
|
|
1587
|
+
it("pastes clipboard files into attachments when no title is set", async () => {
|
|
1347
1588
|
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1348
1589
|
const el = await createElement("note");
|
|
1349
1590
|
const events: CustomEvent[] = [];
|
|
@@ -1379,7 +1620,6 @@ describe("JantComposeEditor", () => {
|
|
|
1379
1620
|
id: "med_test",
|
|
1380
1621
|
});
|
|
1381
1622
|
const el = await createElement("note");
|
|
1382
|
-
el._showTitle = true;
|
|
1383
1623
|
el._title = "Essay";
|
|
1384
1624
|
await el.updateComplete;
|
|
1385
1625
|
|
|
@@ -1399,13 +1639,29 @@ describe("JantComposeEditor", () => {
|
|
|
1399
1639
|
]);
|
|
1400
1640
|
});
|
|
1401
1641
|
|
|
1402
|
-
it("pastes images as attachments when
|
|
1642
|
+
it("pastes images as attachments when only whitespace is in the title", async () => {
|
|
1403
1643
|
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1404
1644
|
const el = await createElement("note");
|
|
1405
|
-
el.
|
|
1406
|
-
el._title = "Essay";
|
|
1645
|
+
el._title = " ";
|
|
1407
1646
|
await el.updateComplete;
|
|
1408
|
-
|
|
1647
|
+
|
|
1648
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1649
|
+
const { handled } = triggerEditorPaste(el, [image]);
|
|
1650
|
+
await el.updateComplete;
|
|
1651
|
+
|
|
1652
|
+
expect(handled).toBe(true);
|
|
1653
|
+
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1654
|
+
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1655
|
+
"clipboard.png",
|
|
1656
|
+
]);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
it("pastes images as attachments in a link, title or no title", async () => {
|
|
1660
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1661
|
+
const el = await createElement("link");
|
|
1662
|
+
// A link's title is required to publish, so it is filled in every link
|
|
1663
|
+
// worth pasting into — it can't be the signal that picks inline.
|
|
1664
|
+
el._title = "An article worth linking";
|
|
1409
1665
|
await el.updateComplete;
|
|
1410
1666
|
|
|
1411
1667
|
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
@@ -1419,11 +1675,12 @@ describe("JantComposeEditor", () => {
|
|
|
1419
1675
|
]);
|
|
1420
1676
|
});
|
|
1421
1677
|
|
|
1422
|
-
it("pastes images as attachments
|
|
1678
|
+
it("pastes images as attachments in a quote carrying a title from a format switch", async () => {
|
|
1423
1679
|
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1424
|
-
const el = await createElement("
|
|
1425
|
-
|
|
1426
|
-
|
|
1680
|
+
const el = await createElement("quote");
|
|
1681
|
+
// A quote has no title field, but switching over from a note leaves one
|
|
1682
|
+
// behind. Nothing the author can see may decide where the image lands.
|
|
1683
|
+
el._title = "Essay";
|
|
1427
1684
|
await el.updateComplete;
|
|
1428
1685
|
|
|
1429
1686
|
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
@@ -1436,4 +1693,242 @@ describe("JantComposeEditor", () => {
|
|
|
1436
1693
|
"clipboard.png",
|
|
1437
1694
|
]);
|
|
1438
1695
|
});
|
|
1696
|
+
|
|
1697
|
+
it("carries an in-flight inline upload across a format switch", async () => {
|
|
1698
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1699
|
+
let finishUpload: (value: { url: string; id: string }) => void = () => {};
|
|
1700
|
+
uploadWithMetadataMock.mockReturnValue(
|
|
1701
|
+
new Promise((resolve) => {
|
|
1702
|
+
finishUpload = resolve;
|
|
1703
|
+
}),
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
const el = await createElement("note");
|
|
1707
|
+
el._title = "Essay";
|
|
1708
|
+
await el.updateComplete;
|
|
1709
|
+
|
|
1710
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1711
|
+
triggerEditorPaste(el, [image]);
|
|
1712
|
+
await el.updateComplete;
|
|
1713
|
+
expect(imageSrcs(el).every((src) => src.startsWith("blob:"))).toBe(true);
|
|
1714
|
+
expect(imageSrcs(el)).toHaveLength(1);
|
|
1715
|
+
|
|
1716
|
+
// The body survives the switch, so the upload watching it has to survive
|
|
1717
|
+
// too — otherwise the blob: placeholder is what gets published.
|
|
1718
|
+
el.format = "quote";
|
|
1719
|
+
await el.updateComplete;
|
|
1720
|
+
await el.updateComplete;
|
|
1721
|
+
|
|
1722
|
+
finishUpload({
|
|
1723
|
+
url: "https://example.test/clipboard.webp",
|
|
1724
|
+
id: "med_test",
|
|
1725
|
+
});
|
|
1726
|
+
await vi.waitFor(() => {
|
|
1727
|
+
expect(imageSrcs(el)).toEqual(["https://example.test/clipboard.webp"]);
|
|
1728
|
+
});
|
|
1729
|
+
expect(bodyImageSrcs(el._bodyJson)).toEqual([
|
|
1730
|
+
"https://example.test/clipboard.webp",
|
|
1731
|
+
]);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
it("drops images into a link as attachments, same as pasting them", async () => {
|
|
1735
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1736
|
+
const el = await createElement("link");
|
|
1737
|
+
el._title = "An article worth linking";
|
|
1738
|
+
await el.updateComplete;
|
|
1739
|
+
|
|
1740
|
+
const image = new File(["image"], "dropped.png", { type: "image/png" });
|
|
1741
|
+
triggerEditorDrop(el, [image]);
|
|
1742
|
+
await el.updateComplete;
|
|
1743
|
+
|
|
1744
|
+
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1745
|
+
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1746
|
+
"dropped.png",
|
|
1747
|
+
]);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it("focuses the body end when blank space around the editor is clicked", async () => {
|
|
1751
|
+
const el = await createElement("note");
|
|
1752
|
+
const editor = requireEditor(el);
|
|
1753
|
+
editor.commands.setContent(
|
|
1754
|
+
tiptapDoc(tiptapParagraph("One two three"), tiptapParagraph("Tail")),
|
|
1755
|
+
);
|
|
1756
|
+
editor.commands.setTextSelection(1);
|
|
1757
|
+
|
|
1758
|
+
const blank = requireElement(
|
|
1759
|
+
el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
|
|
1760
|
+
"expected compose tiptap wrapper",
|
|
1761
|
+
);
|
|
1762
|
+
pressAndClick(blank, blank);
|
|
1763
|
+
|
|
1764
|
+
expect(editor.state.selection.empty).toBe(true);
|
|
1765
|
+
expect(editor.state.selection.from).toBe(editor.state.doc.content.size - 1);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
it("keeps a drag-selection that lifts off the text onto blank space", async () => {
|
|
1769
|
+
const el = await createElement("note");
|
|
1770
|
+
const editor = requireEditor(el);
|
|
1771
|
+
editor.commands.setContent(
|
|
1772
|
+
tiptapDoc(
|
|
1773
|
+
{
|
|
1774
|
+
type: "blockquote",
|
|
1775
|
+
content: [tiptapParagraph("One two three four five six")],
|
|
1776
|
+
},
|
|
1777
|
+
tiptapParagraph("Tail after the quote"),
|
|
1778
|
+
),
|
|
1779
|
+
);
|
|
1780
|
+
// Right-to-left drag across the quote: anchor after "six", head before "One".
|
|
1781
|
+
editor.commands.setTextSelection({ from: 2, to: 29 });
|
|
1782
|
+
|
|
1783
|
+
const quote = requireElement(
|
|
1784
|
+
el.querySelector<HTMLElement>(".compose-tiptap-body blockquote p"),
|
|
1785
|
+
"expected blockquote paragraph",
|
|
1786
|
+
);
|
|
1787
|
+
const blank = requireElement(
|
|
1788
|
+
el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
|
|
1789
|
+
"expected compose tiptap wrapper",
|
|
1790
|
+
);
|
|
1791
|
+
// The pointer went down on the text and came up over the wrapper, so the
|
|
1792
|
+
// browser retargets the click to their common ancestor.
|
|
1793
|
+
pressAndClick(quote, blank, blank);
|
|
1794
|
+
|
|
1795
|
+
expect(editor.state.selection.from).toBe(2);
|
|
1796
|
+
expect(editor.state.selection.to).toBe(29);
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
describe("JantComposeEditor.takeOver", () => {
|
|
1801
|
+
beforeEach(() => {
|
|
1802
|
+
document.body.innerHTML = "";
|
|
1803
|
+
vi.clearAllMocks();
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
/** An editor the dialog is about to render in place of another one. */
|
|
1807
|
+
function unrenderedEditor(format: "note" | "link" | "quote") {
|
|
1808
|
+
const el = document.createElement(
|
|
1809
|
+
"jant-compose-editor",
|
|
1810
|
+
) as JantComposeEditor;
|
|
1811
|
+
el.format = format;
|
|
1812
|
+
el.labels = labels;
|
|
1813
|
+
document.body.appendChild(el);
|
|
1814
|
+
return el;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
it("hands every post field to the editor that replaces it", async () => {
|
|
1818
|
+
// One sample per field the editor holds for its post. The first assertion
|
|
1819
|
+
// keeps this table complete: a field the editor gains without a way across
|
|
1820
|
+
// is how a quote's source link used to vanish on "Add to thread".
|
|
1821
|
+
const samples: Record<string, unknown> = {
|
|
1822
|
+
_title: "Left behind by a format switch",
|
|
1823
|
+
_showTitle: false,
|
|
1824
|
+
_bodyJson: tiptapDoc(tiptapParagraph("My thoughts on it")),
|
|
1825
|
+
_url: "https://example.com/source",
|
|
1826
|
+
_quoteText: "A borrowed sentence.",
|
|
1827
|
+
_quoteAuthor: "Someone Else",
|
|
1828
|
+
_rating: 4,
|
|
1829
|
+
_showRating: true,
|
|
1830
|
+
_attachedTexts: [
|
|
1831
|
+
{
|
|
1832
|
+
clientId: "text-1",
|
|
1833
|
+
bodyJson: tiptapDoc(tiptapParagraph("Attached")),
|
|
1834
|
+
bodyHtml: "",
|
|
1835
|
+
summary: "Attached",
|
|
1836
|
+
},
|
|
1837
|
+
],
|
|
1838
|
+
_attachments: [mediaAttachment("media-1", "med_1")],
|
|
1839
|
+
_attachmentOrder: ["text-1", "media-1"],
|
|
1840
|
+
};
|
|
1841
|
+
const contentProps = (
|
|
1842
|
+
customElements.get("jant-compose-editor") as unknown as {
|
|
1843
|
+
_CONTENT_PROPS: Set<string>;
|
|
1844
|
+
}
|
|
1845
|
+
)._CONTENT_PROPS;
|
|
1846
|
+
expect(Object.keys(samples).sort()).toEqual(
|
|
1847
|
+
[...contentProps, "_attachments"].sort(),
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
const previous = await createElement("quote");
|
|
1851
|
+
for (const [key, value] of Object.entries(samples)) {
|
|
1852
|
+
if (key === "_bodyJson") continue;
|
|
1853
|
+
(previous as unknown as Record<string, unknown>)[key] = value;
|
|
1854
|
+
}
|
|
1855
|
+
requireEditor(previous).commands.setContent(
|
|
1856
|
+
samples._bodyJson as JSONContent,
|
|
1857
|
+
);
|
|
1858
|
+
await previous.updateComplete;
|
|
1859
|
+
// The dialog swaps elements before handing over, so the old one has
|
|
1860
|
+
// already left the DOM — and taken its TipTap instance with it.
|
|
1861
|
+
previous.remove();
|
|
1862
|
+
|
|
1863
|
+
const next = await createElement("quote");
|
|
1864
|
+
next.takeOver(previous);
|
|
1865
|
+
await next.updateComplete;
|
|
1866
|
+
|
|
1867
|
+
for (const [key, value] of Object.entries(samples)) {
|
|
1868
|
+
if (key === "_bodyJson") continue;
|
|
1869
|
+
expect((next as unknown as Record<string, unknown>)[key], key).toEqual(
|
|
1870
|
+
value,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
expect(next.getNormalizedBodyJson()).toEqual(
|
|
1874
|
+
previous.getNormalizedBodyJson(),
|
|
1875
|
+
);
|
|
1876
|
+
expect(requireEditor(next).getText()).toBe("My thoughts on it");
|
|
1877
|
+
expect(next.getData().url).toBe("https://example.com/source");
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
it("keeps a handed-over title toggle through its first render", async () => {
|
|
1881
|
+
const previous = await createElement("note", { titleByDefault: true });
|
|
1882
|
+
previous._showTitle = false;
|
|
1883
|
+
previous.remove();
|
|
1884
|
+
|
|
1885
|
+
const next = unrenderedEditor("note");
|
|
1886
|
+
next.titleByDefault = true;
|
|
1887
|
+
next.takeOver(previous);
|
|
1888
|
+
await next.updateComplete;
|
|
1889
|
+
|
|
1890
|
+
expect(next._showTitle).toBe(false);
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
it.each([
|
|
1894
|
+
["after", true],
|
|
1895
|
+
["before", false],
|
|
1896
|
+
])(
|
|
1897
|
+
"carries an in-flight inline upload to an editor taking over %s its first render",
|
|
1898
|
+
async (_when, renderFirst) => {
|
|
1899
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1900
|
+
let finishUpload: (value: { url: string; id: string }) => void = () => {};
|
|
1901
|
+
uploadWithMetadataMock.mockReturnValue(
|
|
1902
|
+
new Promise((resolve) => {
|
|
1903
|
+
finishUpload = resolve;
|
|
1904
|
+
}),
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
const previous = await createElement("note");
|
|
1908
|
+
previous._title = "Essay";
|
|
1909
|
+
await previous.updateComplete;
|
|
1910
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1911
|
+
triggerEditorPaste(previous, [image]);
|
|
1912
|
+
await previous.updateComplete;
|
|
1913
|
+
expect(imageSrcs(previous)).toHaveLength(1);
|
|
1914
|
+
expect(imageSrcs(previous)[0]).toMatch(/^blob:/);
|
|
1915
|
+
previous.remove();
|
|
1916
|
+
|
|
1917
|
+
const next = unrenderedEditor("note");
|
|
1918
|
+
if (renderFirst) await next.updateComplete;
|
|
1919
|
+
next.takeOver(previous);
|
|
1920
|
+
await next.updateComplete;
|
|
1921
|
+
expect(imageSrcs(next)[0]).toMatch(/^blob:/);
|
|
1922
|
+
|
|
1923
|
+
finishUpload({ url: "https://example.test/clipboard.webp", id: "med_1" });
|
|
1924
|
+
await vi.waitFor(() => {
|
|
1925
|
+
expect(imageSrcs(next)).toEqual([
|
|
1926
|
+
"https://example.test/clipboard.webp",
|
|
1927
|
+
]);
|
|
1928
|
+
});
|
|
1929
|
+
expect(bodyImageSrcs(next._bodyJson)).toEqual([
|
|
1930
|
+
"https://example.test/clipboard.webp",
|
|
1931
|
+
]);
|
|
1932
|
+
},
|
|
1933
|
+
);
|
|
1439
1934
|
});
|