@jackuait/blok 0.4.1-beta.4 → 0.4.1-beta.6
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 +136 -17
- package/codemod/README.md +16 -0
- package/codemod/migrate-editorjs-to-blok.js +868 -92
- package/codemod/test.js +682 -77
- package/dist/blok.mjs +5 -2
- package/dist/chunks/blok-B5qs7C5l.mjs +12838 -0
- package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
- package/dist/chunks/i18next-loader-CTrK3HzG.mjs +43 -0
- package/dist/{index-XWGz4gev.mjs → chunks/index-DDpzQn-0.mjs} +2 -2
- package/dist/chunks/inline-tool-convert-RBcopmCh.mjs +1988 -0
- package/dist/chunks/messages-2434tVOK.mjs +47 -0
- package/dist/chunks/messages-3DcCwXMF.mjs +47 -0
- package/dist/chunks/messages-4kMwVAKY.mjs +47 -0
- package/dist/chunks/messages-57uL5htT.mjs +47 -0
- package/dist/chunks/messages-76-iJV9Q.mjs +47 -0
- package/dist/chunks/messages-8p86Eyf2.mjs +47 -0
- package/dist/chunks/messages-BBX0p0Pi.mjs +47 -0
- package/dist/chunks/messages-BCm2eudQ.mjs +47 -0
- package/dist/chunks/messages-BFiUomgG.mjs +47 -0
- package/dist/chunks/messages-BIPNHHAV.mjs +47 -0
- package/dist/chunks/messages-BUlwu9mo.mjs +47 -0
- package/dist/chunks/messages-BX-DPa-z.mjs +47 -0
- package/dist/chunks/messages-BextV3Qh.mjs +47 -0
- package/dist/chunks/messages-BiPSFlUG.mjs +47 -0
- package/dist/chunks/messages-BiXe9G-O.mjs +47 -0
- package/dist/chunks/messages-Bl5z_Igo.mjs +47 -0
- package/dist/chunks/messages-BnsE97ku.mjs +47 -0
- package/dist/chunks/messages-BoO8gsVD.mjs +47 -0
- package/dist/chunks/messages-BqWaOGMn.mjs +47 -0
- package/dist/chunks/messages-BqkL2_Ro.mjs +47 -0
- package/dist/chunks/messages-BvCkXKX-.mjs +47 -0
- package/dist/chunks/messages-C6tbPLoj.mjs +47 -0
- package/dist/chunks/messages-CA6T3-gQ.mjs +47 -0
- package/dist/chunks/messages-CFFPFdWP.mjs +47 -0
- package/dist/chunks/messages-CFrKE-TN.mjs +47 -0
- package/dist/chunks/messages-CHz8VlG-.mjs +47 -0
- package/dist/chunks/messages-CLixzySl.mjs +47 -0
- package/dist/chunks/messages-CV7OM_qk.mjs +47 -0
- package/dist/chunks/messages-CXHt3eCC.mjs +47 -0
- package/dist/chunks/messages-CbmsBrB0.mjs +47 -0
- package/dist/chunks/messages-Ceo1KtFx.mjs +47 -0
- package/dist/chunks/messages-Cm0LJLtB.mjs +47 -0
- package/dist/chunks/messages-CmymP_Ar.mjs +47 -0
- package/dist/chunks/messages-D0ohMB5H.mjs +47 -0
- package/dist/chunks/messages-D3GrDwXh.mjs +47 -0
- package/dist/chunks/messages-D3vTzIpL.mjs +47 -0
- package/dist/chunks/messages-D5WeksbV.mjs +47 -0
- package/dist/chunks/messages-DGaab4EP.mjs +47 -0
- package/dist/chunks/messages-DKha57ZU.mjs +47 -0
- package/dist/chunks/messages-DOaujgMW.mjs +47 -0
- package/dist/chunks/messages-DVbPLd_0.mjs +47 -0
- package/dist/chunks/messages-D_FCyfW6.mjs +47 -0
- package/dist/chunks/messages-Dd5iZN3c.mjs +47 -0
- package/dist/chunks/messages-DehM7135.mjs +47 -0
- package/dist/chunks/messages-Dg1OHftD.mjs +47 -0
- package/dist/chunks/messages-Di6Flq-b.mjs +47 -0
- package/dist/chunks/messages-Dqhhex6e.mjs +47 -0
- package/dist/chunks/messages-DueVe0F1.mjs +47 -0
- package/dist/chunks/messages-Dx3eFwI0.mjs +47 -0
- package/dist/chunks/messages-FOtiUoKl.mjs +47 -0
- package/dist/chunks/messages-FTOZNhRD.mjs +47 -0
- package/dist/chunks/messages-IQxGfQIV.mjs +47 -0
- package/dist/chunks/messages-JF2fzCkK.mjs +47 -0
- package/dist/chunks/messages-MOGl7I5v.mjs +47 -0
- package/dist/chunks/messages-QgYhPL-3.mjs +47 -0
- package/dist/chunks/messages-WYWIbQwo.mjs +47 -0
- package/dist/chunks/messages-a6A_LgDv.mjs +47 -0
- package/dist/chunks/messages-bSf31LJi.mjs +47 -0
- package/dist/chunks/messages-diGozhTn.mjs +47 -0
- package/dist/chunks/messages-er-kd-VO.mjs +47 -0
- package/dist/chunks/messages-ez3w5NBn.mjs +47 -0
- package/dist/chunks/messages-f3uXjegd.mjs +47 -0
- package/dist/chunks/messages-ohwI1UGv.mjs +47 -0
- package/dist/chunks/messages-p9BZJaFV.mjs +47 -0
- package/dist/chunks/messages-qIQ4L4rw.mjs +47 -0
- package/dist/chunks/messages-qWkXPggi.mjs +47 -0
- package/dist/chunks/messages-w5foGze_.mjs +47 -0
- package/dist/full.mjs +50 -0
- package/dist/locales.mjs +227 -0
- package/dist/messages-2434tVOK.mjs +47 -0
- package/dist/messages-3DcCwXMF.mjs +47 -0
- package/dist/messages-4kMwVAKY.mjs +47 -0
- package/dist/messages-57uL5htT.mjs +47 -0
- package/dist/messages-76-iJV9Q.mjs +47 -0
- package/dist/messages-8p86Eyf2.mjs +47 -0
- package/dist/messages-BBX0p0Pi.mjs +47 -0
- package/dist/messages-BCm2eudQ.mjs +47 -0
- package/dist/messages-BFiUomgG.mjs +47 -0
- package/dist/messages-BIPNHHAV.mjs +47 -0
- package/dist/messages-BUlwu9mo.mjs +47 -0
- package/dist/messages-BX-DPa-z.mjs +47 -0
- package/dist/messages-BextV3Qh.mjs +47 -0
- package/dist/messages-BiPSFlUG.mjs +47 -0
- package/dist/messages-BiXe9G-O.mjs +47 -0
- package/dist/messages-Bl5z_Igo.mjs +47 -0
- package/dist/messages-BnsE97ku.mjs +47 -0
- package/dist/messages-BoO8gsVD.mjs +47 -0
- package/dist/messages-BqWaOGMn.mjs +47 -0
- package/dist/messages-BqkL2_Ro.mjs +47 -0
- package/dist/messages-BvCkXKX-.mjs +47 -0
- package/dist/messages-C6tbPLoj.mjs +47 -0
- package/dist/messages-CA6T3-gQ.mjs +47 -0
- package/dist/messages-CFFPFdWP.mjs +47 -0
- package/dist/messages-CFrKE-TN.mjs +47 -0
- package/dist/messages-CHz8VlG-.mjs +47 -0
- package/dist/messages-CLixzySl.mjs +47 -0
- package/dist/messages-CV7OM_qk.mjs +47 -0
- package/dist/messages-CXHt3eCC.mjs +47 -0
- package/dist/messages-CbmsBrB0.mjs +47 -0
- package/dist/messages-Ceo1KtFx.mjs +47 -0
- package/dist/messages-Cm0LJLtB.mjs +47 -0
- package/dist/messages-CmymP_Ar.mjs +47 -0
- package/dist/messages-D0ohMB5H.mjs +47 -0
- package/dist/messages-D3GrDwXh.mjs +47 -0
- package/dist/messages-D3vTzIpL.mjs +47 -0
- package/dist/messages-D5WeksbV.mjs +47 -0
- package/dist/messages-DGaab4EP.mjs +47 -0
- package/dist/messages-DKha57ZU.mjs +47 -0
- package/dist/messages-DOaujgMW.mjs +47 -0
- package/dist/messages-DVbPLd_0.mjs +47 -0
- package/dist/messages-D_FCyfW6.mjs +47 -0
- package/dist/messages-Dd5iZN3c.mjs +47 -0
- package/dist/messages-DehM7135.mjs +47 -0
- package/dist/messages-Dg1OHftD.mjs +47 -0
- package/dist/messages-Di6Flq-b.mjs +47 -0
- package/dist/messages-Dqhhex6e.mjs +47 -0
- package/dist/messages-DueVe0F1.mjs +47 -0
- package/dist/messages-Dx3eFwI0.mjs +47 -0
- package/dist/messages-FOtiUoKl.mjs +47 -0
- package/dist/messages-FTOZNhRD.mjs +47 -0
- package/dist/messages-IQxGfQIV.mjs +47 -0
- package/dist/messages-JF2fzCkK.mjs +47 -0
- package/dist/messages-MOGl7I5v.mjs +47 -0
- package/dist/messages-QgYhPL-3.mjs +47 -0
- package/dist/messages-WYWIbQwo.mjs +47 -0
- package/dist/messages-a6A_LgDv.mjs +47 -0
- package/dist/messages-bSf31LJi.mjs +47 -0
- package/dist/messages-diGozhTn.mjs +47 -0
- package/dist/messages-er-kd-VO.mjs +47 -0
- package/dist/messages-ez3w5NBn.mjs +47 -0
- package/dist/messages-f3uXjegd.mjs +47 -0
- package/dist/messages-ohwI1UGv.mjs +47 -0
- package/dist/messages-p9BZJaFV.mjs +47 -0
- package/dist/messages-qIQ4L4rw.mjs +47 -0
- package/dist/messages-qWkXPggi.mjs +47 -0
- package/dist/messages-w5foGze_.mjs +47 -0
- package/dist/tools.mjs +3073 -0
- package/dist/vendor.LICENSE.txt +26 -225
- package/package.json +49 -23
- package/src/blok.ts +267 -0
- package/src/components/__module.ts +139 -0
- package/src/components/block/api.ts +155 -0
- package/src/components/block/index.ts +1427 -0
- package/src/components/block-tunes/block-tune-delete.ts +51 -0
- package/src/components/blocks.ts +338 -0
- package/src/components/constants/data-attributes.ts +342 -0
- package/src/components/constants.ts +76 -0
- package/src/components/core.ts +392 -0
- package/src/components/dom.ts +773 -0
- package/src/components/domIterator.ts +189 -0
- package/src/components/errors/critical.ts +5 -0
- package/src/components/events/BlockChanged.ts +16 -0
- package/src/components/events/BlockHovered.ts +21 -0
- package/src/components/events/BlockSettingsClosed.ts +12 -0
- package/src/components/events/BlockSettingsOpened.ts +12 -0
- package/src/components/events/BlokMobileLayoutToggled.ts +15 -0
- package/src/components/events/FakeCursorAboutToBeToggled.ts +17 -0
- package/src/components/events/FakeCursorHaveBeenSet.ts +17 -0
- package/src/components/events/HistoryStateChanged.ts +19 -0
- package/src/components/events/RedactorDomChanged.ts +14 -0
- package/src/components/events/index.ts +46 -0
- package/src/components/flipper.ts +481 -0
- package/src/components/i18n/i18next-loader.ts +84 -0
- package/src/components/i18n/lightweight-i18n.ts +86 -0
- package/src/components/i18n/locales/TRANSLATION_GUIDELINES.md +113 -0
- package/src/components/i18n/locales/am/messages.json +44 -0
- package/src/components/i18n/locales/ar/messages.json +44 -0
- package/src/components/i18n/locales/az/messages.json +44 -0
- package/src/components/i18n/locales/bg/messages.json +44 -0
- package/src/components/i18n/locales/bn/messages.json +44 -0
- package/src/components/i18n/locales/bs/messages.json +44 -0
- package/src/components/i18n/locales/cs/messages.json +44 -0
- package/src/components/i18n/locales/da/messages.json +44 -0
- package/src/components/i18n/locales/de/messages.json +44 -0
- package/src/components/i18n/locales/dv/messages.json +44 -0
- package/src/components/i18n/locales/el/messages.json +44 -0
- package/src/components/i18n/locales/en/messages.json +44 -0
- package/src/components/i18n/locales/es/messages.json +44 -0
- package/src/components/i18n/locales/et/messages.json +44 -0
- package/src/components/i18n/locales/fa/messages.json +44 -0
- package/src/components/i18n/locales/fi/messages.json +44 -0
- package/src/components/i18n/locales/fil/messages.json +44 -0
- package/src/components/i18n/locales/fr/messages.json +44 -0
- package/src/components/i18n/locales/gu/messages.json +44 -0
- package/src/components/i18n/locales/he/messages.json +44 -0
- package/src/components/i18n/locales/hi/messages.json +44 -0
- package/src/components/i18n/locales/hr/messages.json +44 -0
- package/src/components/i18n/locales/hu/messages.json +44 -0
- package/src/components/i18n/locales/hy/messages.json +44 -0
- package/src/components/i18n/locales/id/messages.json +44 -0
- package/src/components/i18n/locales/index.ts +225 -0
- package/src/components/i18n/locales/it/messages.json +44 -0
- package/src/components/i18n/locales/ja/messages.json +44 -0
- package/src/components/i18n/locales/ka/messages.json +44 -0
- package/src/components/i18n/locales/km/messages.json +44 -0
- package/src/components/i18n/locales/kn/messages.json +44 -0
- package/src/components/i18n/locales/ko/messages.json +44 -0
- package/src/components/i18n/locales/ku/messages.json +44 -0
- package/src/components/i18n/locales/lo/messages.json +44 -0
- package/src/components/i18n/locales/lt/messages.json +44 -0
- package/src/components/i18n/locales/lv/messages.json +44 -0
- package/src/components/i18n/locales/mk/messages.json +44 -0
- package/src/components/i18n/locales/ml/messages.json +44 -0
- package/src/components/i18n/locales/mn/messages.json +44 -0
- package/src/components/i18n/locales/mr/messages.json +44 -0
- package/src/components/i18n/locales/ms/messages.json +44 -0
- package/src/components/i18n/locales/my/messages.json +44 -0
- package/src/components/i18n/locales/ne/messages.json +44 -0
- package/src/components/i18n/locales/nl/messages.json +44 -0
- package/src/components/i18n/locales/no/messages.json +44 -0
- package/src/components/i18n/locales/pa/messages.json +44 -0
- package/src/components/i18n/locales/pl/messages.json +44 -0
- package/src/components/i18n/locales/ps/messages.json +44 -0
- package/src/components/i18n/locales/pt/messages.json +44 -0
- package/src/components/i18n/locales/ro/messages.json +44 -0
- package/src/components/i18n/locales/ru/messages.json +44 -0
- package/src/components/i18n/locales/sd/messages.json +44 -0
- package/src/components/i18n/locales/si/messages.json +44 -0
- package/src/components/i18n/locales/sk/messages.json +44 -0
- package/src/components/i18n/locales/sl/messages.json +44 -0
- package/src/components/i18n/locales/sq/messages.json +44 -0
- package/src/components/i18n/locales/sr/messages.json +44 -0
- package/src/components/i18n/locales/sv/messages.json +44 -0
- package/src/components/i18n/locales/sw/messages.json +44 -0
- package/src/components/i18n/locales/ta/messages.json +44 -0
- package/src/components/i18n/locales/te/messages.json +44 -0
- package/src/components/i18n/locales/th/messages.json +44 -0
- package/src/components/i18n/locales/tr/messages.json +44 -0
- package/src/components/i18n/locales/ug/messages.json +44 -0
- package/src/components/i18n/locales/uk/messages.json +44 -0
- package/src/components/i18n/locales/ur/messages.json +44 -0
- package/src/components/i18n/locales/vi/messages.json +44 -0
- package/src/components/i18n/locales/yi/messages.json +44 -0
- package/src/components/i18n/locales/zh/messages.json +44 -0
- package/src/components/icons/index.ts +242 -0
- package/src/components/inline-tools/inline-tool-bold.ts +2213 -0
- package/src/components/inline-tools/inline-tool-convert.ts +141 -0
- package/src/components/inline-tools/inline-tool-italic.ts +500 -0
- package/src/components/inline-tools/inline-tool-link.ts +539 -0
- package/src/components/modules/api/blocks.ts +363 -0
- package/src/components/modules/api/caret.ts +125 -0
- package/src/components/modules/api/events.ts +51 -0
- package/src/components/modules/api/history.ts +73 -0
- package/src/components/modules/api/i18n.ts +33 -0
- package/src/components/modules/api/index.ts +39 -0
- package/src/components/modules/api/inlineToolbar.ts +33 -0
- package/src/components/modules/api/listeners.ts +56 -0
- package/src/components/modules/api/notifier.ts +46 -0
- package/src/components/modules/api/readonly.ts +39 -0
- package/src/components/modules/api/sanitizer.ts +30 -0
- package/src/components/modules/api/saver.ts +52 -0
- package/src/components/modules/api/selection.ts +48 -0
- package/src/components/modules/api/styles.ts +72 -0
- package/src/components/modules/api/toolbar.ts +79 -0
- package/src/components/modules/api/tools.ts +16 -0
- package/src/components/modules/api/tooltip.ts +67 -0
- package/src/components/modules/api/ui.ts +36 -0
- package/src/components/modules/blockEvents.ts +1375 -0
- package/src/components/modules/blockManager.ts +1348 -0
- package/src/components/modules/blockSelection.ts +708 -0
- package/src/components/modules/caret.ts +853 -0
- package/src/components/modules/crossBlockSelection.ts +329 -0
- package/src/components/modules/dragManager.ts +1141 -0
- package/src/components/modules/history.ts +1098 -0
- package/src/components/modules/i18n.ts +325 -0
- package/src/components/modules/index.ts +139 -0
- package/src/components/modules/modificationsObserver.ts +147 -0
- package/src/components/modules/paste.ts +1092 -0
- package/src/components/modules/readonly.ts +136 -0
- package/src/components/modules/rectangleSelection.ts +668 -0
- package/src/components/modules/renderer.ts +155 -0
- package/src/components/modules/saver.ts +283 -0
- package/src/components/modules/toolbar/blockSettings.ts +776 -0
- package/src/components/modules/toolbar/index.ts +1311 -0
- package/src/components/modules/toolbar/inline.ts +956 -0
- package/src/components/modules/tools.ts +589 -0
- package/src/components/modules/ui.ts +1179 -0
- package/src/components/polyfills.ts +113 -0
- package/src/components/selection.ts +1189 -0
- package/src/components/tools/base.ts +274 -0
- package/src/components/tools/block.ts +291 -0
- package/src/components/tools/collection.ts +67 -0
- package/src/components/tools/factory.ts +85 -0
- package/src/components/tools/inline.ts +71 -0
- package/src/components/tools/tune.ts +33 -0
- package/src/components/ui/toolbox.ts +497 -0
- package/src/components/utils/announcer.ts +205 -0
- package/src/components/utils/api.ts +20 -0
- package/src/components/utils/bem.ts +26 -0
- package/src/components/utils/blocks.ts +284 -0
- package/src/components/utils/caret.ts +1067 -0
- package/src/components/utils/data-model-transform.ts +382 -0
- package/src/components/utils/events.ts +117 -0
- package/src/components/utils/keyboard.ts +60 -0
- package/src/components/utils/listeners.ts +296 -0
- package/src/components/utils/mutations.ts +39 -0
- package/src/components/utils/notifier/draw.ts +190 -0
- package/src/components/utils/notifier/index.ts +66 -0
- package/src/components/utils/notifier/types.ts +1 -0
- package/src/components/utils/notifier.ts +77 -0
- package/src/components/utils/placeholder.ts +140 -0
- package/src/components/utils/popover/components/hint/hint.const.ts +10 -0
- package/src/components/utils/popover/components/hint/hint.ts +46 -0
- package/src/components/utils/popover/components/hint/index.ts +6 -0
- package/src/components/utils/popover/components/popover-header/index.ts +2 -0
- package/src/components/utils/popover/components/popover-header/popover-header.const.ts +8 -0
- package/src/components/utils/popover/components/popover-header/popover-header.ts +80 -0
- package/src/components/utils/popover/components/popover-header/popover-header.types.ts +14 -0
- package/src/components/utils/popover/components/popover-item/index.ts +13 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +50 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +666 -0
- package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts +14 -0
- package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts +136 -0
- package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +20 -0
- package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts +117 -0
- package/src/components/utils/popover/components/popover-item/popover-item.ts +187 -0
- package/src/components/utils/popover/components/search-input/index.ts +2 -0
- package/src/components/utils/popover/components/search-input/search-input.const.ts +8 -0
- package/src/components/utils/popover/components/search-input/search-input.ts +181 -0
- package/src/components/utils/popover/components/search-input/search-input.types.ts +30 -0
- package/src/components/utils/popover/index.ts +13 -0
- package/src/components/utils/popover/popover-abstract.ts +448 -0
- package/src/components/utils/popover/popover-desktop.ts +643 -0
- package/src/components/utils/popover/popover-inline.ts +338 -0
- package/src/components/utils/popover/popover-mobile.ts +201 -0
- package/src/components/utils/popover/popover.const.ts +81 -0
- package/src/components/utils/popover/utils/popover-states-history.ts +72 -0
- package/src/components/utils/promise-queue.ts +43 -0
- package/src/components/utils/sanitizer.ts +537 -0
- package/src/components/utils/scroll-locker.ts +87 -0
- package/src/components/utils/shortcut.ts +231 -0
- package/src/components/utils/shortcuts.ts +113 -0
- package/src/components/utils/tools.ts +105 -0
- package/src/components/utils/tooltip.ts +642 -0
- package/src/components/utils/tw.ts +241 -0
- package/src/components/utils.ts +1081 -0
- package/src/env.d.ts +13 -0
- package/src/full.ts +69 -0
- package/src/locales.ts +51 -0
- package/src/stories/Block.stories.ts +498 -0
- package/src/stories/EditorModes.stories.ts +505 -0
- package/src/stories/Header.stories.ts +137 -0
- package/src/stories/InlineToolbar.stories.ts +498 -0
- package/src/stories/List.stories.ts +259 -0
- package/src/stories/Notifier.stories.ts +340 -0
- package/src/stories/Paragraph.stories.ts +112 -0
- package/src/stories/Placeholder.stories.ts +319 -0
- package/src/stories/Popover.stories.ts +844 -0
- package/src/stories/Selection.stories.ts +250 -0
- package/src/stories/StubBlock.stories.ts +156 -0
- package/src/stories/Toolbar.stories.ts +223 -0
- package/src/stories/Toolbox.stories.ts +166 -0
- package/src/stories/Tooltip.stories.ts +198 -0
- package/src/stories/helpers.ts +463 -0
- package/src/styles/main.css +123 -0
- package/src/tools/header/index.ts +570 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/list/index.ts +1803 -0
- package/src/tools/paragraph/index.ts +411 -0
- package/src/tools/stub/index.ts +107 -0
- package/src/types-internal/blok-modules.d.ts +87 -0
- package/src/types-internal/html-janitor.d.ts +28 -0
- package/src/types-internal/module-config.d.ts +11 -0
- package/src/variants/all-locales.ts +155 -0
- package/src/variants/blok-maximum.ts +20 -0
- package/src/variants/blok-minimum.ts +243 -0
- package/types/api/blocks.d.ts +1 -1
- package/types/api/i18n.d.ts +5 -3
- package/types/api/selection.d.ts +6 -0
- package/types/api/styles.d.ts +23 -10
- package/types/configs/blok-config.d.ts +29 -0
- package/types/configs/i18n-config.d.ts +52 -2
- package/types/configs/i18n-dictionary.d.ts +16 -90
- package/types/data-attributes.d.ts +169 -0
- package/types/data-formats/output-data.d.ts +15 -0
- package/types/full.d.ts +80 -0
- package/types/index.d.ts +9 -12
- package/types/locales.d.ts +59 -0
- package/types/tools/adapters/inline-tool-adapter.d.ts +10 -0
- package/types/tools/block-tool.d.ts +9 -0
- package/types/tools/header.d.ts +18 -0
- package/types/tools/index.d.ts +1 -0
- package/types/tools/list.d.ts +91 -0
- package/types/tools/paragraph.d.ts +71 -0
- package/types/tools/tool-settings.d.ts +16 -2
- package/types/tools/tool.d.ts +6 -0
- package/types/tools-entry.d.ts +49 -0
- package/types/utils/popover/popover-item.d.ts +6 -5
- package/dist/blok-B870U2fw.mjs +0 -25803
- package/dist/blok.umd.js +0 -181
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
import { Dom as $, isCollapsedWhitespaces } from '../dom';
|
|
2
|
+
|
|
3
|
+
const NBSP_CHAR = '\u00A0';
|
|
4
|
+
|
|
5
|
+
const whitespaceFollowingRemovedEmptyInline = new WeakSet<Text>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns TextNode containing a caret and a caret offset in it
|
|
9
|
+
* Returns null if there is no caret set
|
|
10
|
+
*
|
|
11
|
+
* Handles a case when focusNode is an ElementNode and focusOffset is a child index,
|
|
12
|
+
* returns child node with focusOffset index as a new focusNode
|
|
13
|
+
*/
|
|
14
|
+
export const getCaretNodeAndOffset = (): [Node | null, number] => {
|
|
15
|
+
const selection = window.getSelection();
|
|
16
|
+
|
|
17
|
+
if (selection === null) {
|
|
18
|
+
return [null, 0];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const initialFocusNode = selection.focusNode;
|
|
22
|
+
const initialFocusOffset = selection.focusOffset;
|
|
23
|
+
|
|
24
|
+
if (initialFocusNode === null) {
|
|
25
|
+
return [null, 0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Case when focusNode is an Element (or Document). In this case, focusOffset is a child index.
|
|
30
|
+
* We need to return child with focusOffset index as a new focusNode.
|
|
31
|
+
*
|
|
32
|
+
* <div>|hello</div> <---- Selection references to <div> instead of text node
|
|
33
|
+
*
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
if (initialFocusNode.nodeType === Node.TEXT_NODE || initialFocusNode.childNodes.length === 0) {
|
|
37
|
+
return [initialFocusNode, initialFocusOffset];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* In normal cases, focusOffset is a child index.
|
|
42
|
+
*/
|
|
43
|
+
const regularChild = initialFocusNode.childNodes[initialFocusOffset];
|
|
44
|
+
|
|
45
|
+
if (regularChild !== undefined) {
|
|
46
|
+
return [regularChild, 0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* But in Firefox, focusOffset can be 1 with the single child.
|
|
51
|
+
*/
|
|
52
|
+
const fallbackChild = initialFocusNode.childNodes[initialFocusOffset - 1] ?? null;
|
|
53
|
+
const textContent = fallbackChild?.textContent ?? null;
|
|
54
|
+
|
|
55
|
+
return [fallbackChild, textContent !== null ? textContent.length : 0];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isElementVisuallyEmpty = (element: Element): boolean => {
|
|
59
|
+
if (!(element instanceof HTMLElement)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ($.isSingleTag(element) || $.isNativeInput(element)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (element.childNodes.length === 0) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const textContent = element.textContent ?? '';
|
|
72
|
+
|
|
73
|
+
if (textContent.includes(NBSP_CHAR)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!isCollapsedWhitespaces(textContent)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Array.from(element.children).every((child) => {
|
|
82
|
+
return isElementVisuallyEmpty(child);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const inlineRemovalObserver = typeof window !== 'undefined' && typeof window.MutationObserver !== 'undefined'
|
|
87
|
+
? new window.MutationObserver((records) => {
|
|
88
|
+
for (const record of records) {
|
|
89
|
+
const referenceNextSibling = record.nextSibling;
|
|
90
|
+
|
|
91
|
+
record.removedNodes.forEach((node) => {
|
|
92
|
+
if (!(node instanceof Element)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isElementVisuallyEmpty(node)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const candidate = referenceNextSibling instanceof Text ? referenceNextSibling : null;
|
|
101
|
+
|
|
102
|
+
if (candidate === null) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!candidate.isConnected) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parentElement = candidate.parentElement;
|
|
111
|
+
|
|
112
|
+
if (!(parentElement?.isContentEditable ?? false)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const firstChar = candidate.textContent?.[0] ?? null;
|
|
117
|
+
const isWhitespace = firstChar === NBSP_CHAR || firstChar === ' ';
|
|
118
|
+
|
|
119
|
+
if (!isWhitespace) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
whitespaceFollowingRemovedEmptyInline.add(candidate);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
: null;
|
|
128
|
+
|
|
129
|
+
const observedDocuments = new WeakSet<Document>();
|
|
130
|
+
|
|
131
|
+
const ensureInlineRemovalObserver = (doc: Document): void => {
|
|
132
|
+
if (inlineRemovalObserver === null || observedDocuments.has(doc)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const startObserving = (): void => {
|
|
137
|
+
if (doc.body === null) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
inlineRemovalObserver.observe(doc.body, {
|
|
142
|
+
childList: true,
|
|
143
|
+
subtree: true,
|
|
144
|
+
});
|
|
145
|
+
observedDocuments.add(doc);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (doc.readyState === 'loading') {
|
|
149
|
+
doc.addEventListener('DOMContentLoaded', startObserving, { once: true });
|
|
150
|
+
} else {
|
|
151
|
+
startObserving();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
|
|
156
|
+
ensureInlineRemovalObserver(window.document);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const findNbspAfterEmptyInline = (root: HTMLElement): { node: Text; offset: number } | null => {
|
|
160
|
+
ensureInlineRemovalObserver(root.ownerDocument);
|
|
161
|
+
|
|
162
|
+
const [caretNode, caretOffset] = getCaretNodeAndOffset();
|
|
163
|
+
|
|
164
|
+
if (caretNode === null || !root.contains(caretNode)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (caretNode.nodeType === Node.TEXT_NODE && caretOffset < ((caretNode.textContent ?? '').length)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
173
|
+
|
|
174
|
+
walker.currentNode = caretNode;
|
|
175
|
+
|
|
176
|
+
for (; ;) {
|
|
177
|
+
const nextTextNode = walker.nextNode() as Text | null;
|
|
178
|
+
|
|
179
|
+
if (nextTextNode === null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const textContent = nextTextNode.textContent ?? '';
|
|
184
|
+
|
|
185
|
+
if (textContent.length === 0) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const firstChar = textContent[0];
|
|
190
|
+
const isTargetWhitespace = firstChar === NBSP_CHAR || firstChar === ' ';
|
|
191
|
+
|
|
192
|
+
if (!isTargetWhitespace) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (nextTextNode === caretNode) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pathRange = document.createRange();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
pathRange.setStart(caretNode, caretOffset);
|
|
204
|
+
pathRange.setEnd(nextTextNode, 0);
|
|
205
|
+
} catch (_error) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const betweenFragment = pathRange.cloneContents();
|
|
210
|
+
const container = document.createElement('div');
|
|
211
|
+
|
|
212
|
+
container.appendChild(betweenFragment);
|
|
213
|
+
|
|
214
|
+
const hasEmptyElementBetween = Array.from(container.querySelectorAll('*')).some((element) => {
|
|
215
|
+
const text = element.textContent ?? '';
|
|
216
|
+
|
|
217
|
+
return text.length === 0 || isCollapsedWhitespaces(text);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const wasEmptyInlineRemoved = whitespaceFollowingRemovedEmptyInline.has(nextTextNode);
|
|
221
|
+
|
|
222
|
+
if (!hasEmptyElementBetween && !wasEmptyInlineRemoved) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (wasEmptyInlineRemoved) {
|
|
227
|
+
whitespaceFollowingRemovedEmptyInline.delete(nextTextNode);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
node: nextTextNode,
|
|
232
|
+
offset: 0,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Checks content at left or right of the passed node for emptiness.
|
|
239
|
+
* @param contenteditable - The contenteditable element containing the nodes.
|
|
240
|
+
* @param fromNode - The starting node to check from.
|
|
241
|
+
* @param offsetInsideNode - The offset inside the starting node.
|
|
242
|
+
* @param direction - The direction to check ('left' or 'right').
|
|
243
|
+
* @returns true if adjacent content is empty, false otherwise.
|
|
244
|
+
*/
|
|
245
|
+
export const checkContenteditableSliceForEmptiness = (contenteditable: HTMLElement, fromNode: Node, offsetInsideNode: number, direction: 'left' | 'right'): boolean => {
|
|
246
|
+
const range = document.createRange();
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* In case of "left":
|
|
250
|
+
* Set range from the start of the contenteditable to the passed offset
|
|
251
|
+
*/
|
|
252
|
+
if (direction === 'left') {
|
|
253
|
+
range.selectNodeContents(contenteditable);
|
|
254
|
+
range.setEnd(fromNode, offsetInsideNode);
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* In case of "right":
|
|
258
|
+
* Set range from the passed offset to the end of the contenteditable
|
|
259
|
+
*/
|
|
260
|
+
} else {
|
|
261
|
+
range.selectNodeContents(contenteditable);
|
|
262
|
+
range.setStart(fromNode, offsetInsideNode);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clone the range's content and check its text content
|
|
267
|
+
*/
|
|
268
|
+
const clonedContent = range.cloneContents();
|
|
269
|
+
const tempDiv = document.createElement('div');
|
|
270
|
+
|
|
271
|
+
tempDiv.appendChild(clonedContent);
|
|
272
|
+
|
|
273
|
+
const textContent = tempDiv.textContent || '';
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if we have any tags in the slice
|
|
277
|
+
* We should not ignore them to allow navigation inside (e.g. empty bold tag)
|
|
278
|
+
*/
|
|
279
|
+
const hasSignificantTags = tempDiv.querySelectorAll('img, br, hr, input, area, base, col, embed, link, meta, param, source, track, wbr').length > 0;
|
|
280
|
+
|
|
281
|
+
if (hasSignificantTags) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if there is a non-breaking space,
|
|
287
|
+
* since textContent can replace it with a space
|
|
288
|
+
*/
|
|
289
|
+
const hasNbsp = textContent.includes('\u00A0') || tempDiv.innerHTML.includes(' ') || range.toString().includes('\u00A0');
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if we have NBSP in the text node itself (if fromNode is text node)
|
|
293
|
+
* This avoids issues with range.toString() normalization
|
|
294
|
+
*/
|
|
295
|
+
const isNBSPInTextNode = fromNode.nodeType === Node.TEXT_NODE &&
|
|
296
|
+
(direction === 'left'
|
|
297
|
+
? (fromNode.textContent || '').slice(0, offsetInsideNode)
|
|
298
|
+
: (fromNode.textContent || '').slice(offsetInsideNode)
|
|
299
|
+
).includes('\u00A0');
|
|
300
|
+
|
|
301
|
+
if (hasNbsp || isNBSPInTextNode) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check for visual width
|
|
307
|
+
* This helps to detect that might be converted to regular space in textContent but still renders with width
|
|
308
|
+
*/
|
|
309
|
+
tempDiv.style.position = 'absolute';
|
|
310
|
+
tempDiv.style.visibility = 'hidden';
|
|
311
|
+
tempDiv.style.height = 'auto';
|
|
312
|
+
tempDiv.style.width = 'auto';
|
|
313
|
+
tempDiv.style.whiteSpace = window.getComputedStyle(contenteditable).whiteSpace;
|
|
314
|
+
|
|
315
|
+
document.body.appendChild(tempDiv);
|
|
316
|
+
const width = tempDiv.getBoundingClientRect().width;
|
|
317
|
+
|
|
318
|
+
document.body.removeChild(tempDiv);
|
|
319
|
+
|
|
320
|
+
if (width > 0) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* In HTML there are two types of whitespaces:
|
|
326
|
+
* - visible ( )
|
|
327
|
+
* - invisible (trailing spaces, tabs, etc.)
|
|
328
|
+
*
|
|
329
|
+
* If text contains only invisible whitespaces, it is considered to be empty
|
|
330
|
+
*/
|
|
331
|
+
if (!isCollapsedWhitespaces(textContent)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const style = window.getComputedStyle(contenteditable);
|
|
336
|
+
const isPre = style.whiteSpace.startsWith('pre');
|
|
337
|
+
|
|
338
|
+
if (isPre && textContent.length > 0) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return true;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Checks if caret is at the start of the passed input
|
|
347
|
+
*
|
|
348
|
+
* Cases:
|
|
349
|
+
* Native input:
|
|
350
|
+
* - if offset is 0, caret is at the start
|
|
351
|
+
* Contenteditable:
|
|
352
|
+
* - caret at the first text node and offset is 0 — caret is at the start
|
|
353
|
+
* - caret not at the first text node — we need to check left siblings for emptiness
|
|
354
|
+
* - caret offset > 0, but all left part is visible (nbsp) — caret is not at the start
|
|
355
|
+
* - caret offset > 0, but all left part is invisible (whitespaces) — caret is at the start
|
|
356
|
+
* @param input - input where caret should be checked
|
|
357
|
+
*/
|
|
358
|
+
export const isCaretAtStartOfInput = (input: HTMLElement): boolean => {
|
|
359
|
+
const firstNode = $.getDeepestNode(input);
|
|
360
|
+
|
|
361
|
+
if (firstNode === null || $.isEmpty(input)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* In case of native input, we simply check if offset is 0
|
|
367
|
+
*/
|
|
368
|
+
if ($.isNativeInput(firstNode)) {
|
|
369
|
+
return (firstNode as HTMLInputElement).selectionEnd === 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if ($.isEmpty(input)) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const [caretNode, caretOffset] = getCaretNodeAndOffset();
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* If there is no selection, caret is not at the start
|
|
380
|
+
*/
|
|
381
|
+
if (caretNode === null) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* If caret is inside a nested tag (e.g. <b>), we should let browser handle the navigation
|
|
387
|
+
* to exit the tag first, before moving to the previous block.
|
|
388
|
+
*/
|
|
389
|
+
const selection = window.getSelection();
|
|
390
|
+
const focusNode = selection?.focusNode ?? null;
|
|
391
|
+
|
|
392
|
+
if (focusNode !== null && focusNode !== input && !(focusNode.nodeType === Node.TEXT_NODE && focusNode.parentNode === input)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* If there is nothing visible to the left of the caret, it is considered to be at the start
|
|
398
|
+
*/
|
|
399
|
+
return checkContenteditableSliceForEmptiness(input, caretNode, caretOffset, 'left');
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Checks if caret is at the end of the passed input
|
|
404
|
+
*
|
|
405
|
+
* Cases:
|
|
406
|
+
* Native input:
|
|
407
|
+
* - if offset is equal to value length, caret is at the end
|
|
408
|
+
* Contenteditable:
|
|
409
|
+
* - caret at the last text node and offset is equal to text length — caret is at the end
|
|
410
|
+
* - caret not at the last text node — we need to check right siblings for emptiness
|
|
411
|
+
* - caret offset < text length, but all right part is visible (nbsp) — caret is at the end
|
|
412
|
+
* - caret offset < text length, but all right part is invisible (whitespaces) — caret is at the end
|
|
413
|
+
* @param input - input where caret should be checked
|
|
414
|
+
*/
|
|
415
|
+
export const isCaretAtEndOfInput = (input: HTMLElement): boolean => {
|
|
416
|
+
const lastNode = $.getDeepestNode(input, true);
|
|
417
|
+
|
|
418
|
+
if (lastNode === null) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* In case of native input, we simply check if offset is equal to value length
|
|
424
|
+
*/
|
|
425
|
+
if ($.isNativeInput(lastNode)) {
|
|
426
|
+
return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const [caretNode, caretOffset] = getCaretNodeAndOffset();
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* If there is no selection, caret is not at the end
|
|
433
|
+
*/
|
|
434
|
+
if (caretNode === null) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* If there is nothing visible to the right of the caret, it is considered to be at the end
|
|
440
|
+
*/
|
|
441
|
+
return checkContenteditableSliceForEmptiness(input, caretNode, caretOffset, 'right');
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Gets a valid DOMRect for the caret position.
|
|
446
|
+
* Falls back to container element rect or input rect if the caret rect has no dimensions.
|
|
447
|
+
*/
|
|
448
|
+
const getValidCaretRect = (range: Range, input: HTMLElement): DOMRect => {
|
|
449
|
+
const caretRect = range.getBoundingClientRect();
|
|
450
|
+
|
|
451
|
+
if (caretRect.height !== 0 || caretRect.top !== 0) {
|
|
452
|
+
return caretRect;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const container = range.startContainer;
|
|
456
|
+
const element = container.nodeType === Node.ELEMENT_NODE
|
|
457
|
+
? container as HTMLElement
|
|
458
|
+
: container.parentElement;
|
|
459
|
+
|
|
460
|
+
if (!element) {
|
|
461
|
+
return input.getBoundingClientRect();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const elementRect = element.getBoundingClientRect();
|
|
465
|
+
|
|
466
|
+
if (elementRect.height !== 0 || elementRect.top !== 0) {
|
|
467
|
+
return elementRect;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return input.getBoundingClientRect();
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Checks if the caret is at the first (top) line of a multi-line input.
|
|
475
|
+
* This is used for Notion-style navigation where Arrow Up should only
|
|
476
|
+
* move to the previous block when the caret can't move up within the current block.
|
|
477
|
+
*
|
|
478
|
+
* @param input - the contenteditable element or native input to check
|
|
479
|
+
* @returns true if caret is at the first line (or input is single-line)
|
|
480
|
+
*/
|
|
481
|
+
export const isCaretAtFirstLine = (input: HTMLElement): boolean => {
|
|
482
|
+
/**
|
|
483
|
+
* For single-line native inputs, always return true
|
|
484
|
+
*/
|
|
485
|
+
if ($.isNativeInput(input) && input.tagName === 'INPUT') {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* For textarea, check if cursor is before the first newline
|
|
491
|
+
*/
|
|
492
|
+
if ($.isNativeInput(input)) {
|
|
493
|
+
const nativeInput = input as HTMLTextAreaElement;
|
|
494
|
+
const selectionStart = nativeInput.selectionStart ?? 0;
|
|
495
|
+
const textBeforeCursor = nativeInput.value.substring(0, selectionStart);
|
|
496
|
+
|
|
497
|
+
return !textBeforeCursor.includes('\n');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const selection = window.getSelection();
|
|
501
|
+
|
|
502
|
+
if (!selection || selection.rangeCount === 0) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const range = selection.getRangeAt(0);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get a valid caret rect, with fallbacks for zero-dimension rects
|
|
510
|
+
*/
|
|
511
|
+
const caretRect = getValidCaretRect(range, input);
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get the first line's position by creating a range at the start of the input
|
|
515
|
+
*/
|
|
516
|
+
const firstNode = $.getDeepestNode(input, false);
|
|
517
|
+
|
|
518
|
+
if (!firstNode) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const firstLineRange = document.createRange();
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
firstLineRange.setStart(firstNode, 0);
|
|
526
|
+
firstLineRange.setEnd(firstNode, 0);
|
|
527
|
+
} catch {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const firstLineRect = firstLineRange.getBoundingClientRect();
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* If the first line rect has no dimensions, fall back to input's top
|
|
535
|
+
*/
|
|
536
|
+
if (firstLineRect.height === 0 && firstLineRect.top === 0) {
|
|
537
|
+
const inputRect = input.getBoundingClientRect();
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Consider caret at first line if it's within the first line height from top
|
|
541
|
+
* Use a threshold based on typical line height
|
|
542
|
+
*/
|
|
543
|
+
const lineHeight = parseFloat(window.getComputedStyle(input).lineHeight) || 20;
|
|
544
|
+
|
|
545
|
+
return caretRect.top < inputRect.top + lineHeight;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Compare the vertical positions - if caret is on the same line as the first character,
|
|
550
|
+
* they should have similar top values (within a small threshold for rounding)
|
|
551
|
+
*/
|
|
552
|
+
const threshold = 5; // pixels tolerance for line comparison
|
|
553
|
+
|
|
554
|
+
return Math.abs(caretRect.top - firstLineRect.top) < threshold;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Checks if the caret is at the last (bottom) line of a multi-line input.
|
|
559
|
+
* This is used for Notion-style navigation where Arrow Down should only
|
|
560
|
+
* move to the next block when the caret can't move down within the current block.
|
|
561
|
+
*
|
|
562
|
+
* @param input - the contenteditable element or native input to check
|
|
563
|
+
* @returns true if caret is at the last line (or input is single-line)
|
|
564
|
+
*/
|
|
565
|
+
export const isCaretAtLastLine = (input: HTMLElement): boolean => {
|
|
566
|
+
/**
|
|
567
|
+
* For single-line native inputs, always return true
|
|
568
|
+
*/
|
|
569
|
+
if ($.isNativeInput(input) && input.tagName === 'INPUT') {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* For textarea, check if cursor is after the last newline
|
|
575
|
+
*/
|
|
576
|
+
if ($.isNativeInput(input)) {
|
|
577
|
+
const nativeInput = input as HTMLTextAreaElement;
|
|
578
|
+
const selectionEnd = nativeInput.selectionEnd ?? nativeInput.value.length;
|
|
579
|
+
const textAfterCursor = nativeInput.value.substring(selectionEnd);
|
|
580
|
+
|
|
581
|
+
return !textAfterCursor.includes('\n');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const selection = window.getSelection();
|
|
585
|
+
|
|
586
|
+
if (!selection || selection.rangeCount === 0) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const range = selection.getRangeAt(0);
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get a valid caret rect, with fallbacks for zero-dimension rects
|
|
594
|
+
*/
|
|
595
|
+
const caretRect = getValidCaretRect(range, input);
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get the last line's position by creating a range at the end of the input
|
|
599
|
+
*/
|
|
600
|
+
const lastNode = $.getDeepestNode(input, true);
|
|
601
|
+
|
|
602
|
+
if (!lastNode) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const lastLineRange = document.createRange();
|
|
607
|
+
const lastNodeLength = $.getContentLength(lastNode);
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
lastLineRange.setStart(lastNode, lastNodeLength);
|
|
611
|
+
lastLineRange.setEnd(lastNode, lastNodeLength);
|
|
612
|
+
} catch {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const lastLineRect = lastLineRange.getBoundingClientRect();
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* If the last line rect has no dimensions, fall back to input's bottom
|
|
620
|
+
*/
|
|
621
|
+
if (lastLineRect.height === 0 && lastLineRect.bottom === 0) {
|
|
622
|
+
const inputRect = input.getBoundingClientRect();
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Consider caret at last line if it's within the last line height from bottom
|
|
626
|
+
* Use a threshold based on typical line height
|
|
627
|
+
*/
|
|
628
|
+
const lineHeight = parseFloat(window.getComputedStyle(input).lineHeight) || 20;
|
|
629
|
+
|
|
630
|
+
return caretRect.bottom > inputRect.bottom - lineHeight;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Compare the vertical positions - if caret is on the same line as the last character,
|
|
635
|
+
* they should have similar bottom values (within a small threshold for rounding)
|
|
636
|
+
*/
|
|
637
|
+
const threshold = 5; // pixels tolerance for line comparison
|
|
638
|
+
|
|
639
|
+
return Math.abs(caretRect.bottom - lastLineRect.bottom) < threshold;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Set focus to contenteditable or native input element
|
|
644
|
+
* @param element - element where to set focus
|
|
645
|
+
* @param atStart - where to set focus: at the start or at the end
|
|
646
|
+
*/
|
|
647
|
+
export const focus = (element: HTMLElement, atStart = true): void => {
|
|
648
|
+
/** If element is native input */
|
|
649
|
+
if ($.isNativeInput(element)) {
|
|
650
|
+
element.focus();
|
|
651
|
+
const position = atStart ? 0 : element.value.length;
|
|
652
|
+
|
|
653
|
+
element.setSelectionRange(position, position);
|
|
654
|
+
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Focus the contenteditable element to ensure caret is visible.
|
|
660
|
+
* Without focus, the selection range can be set but the caret won't be visible.
|
|
661
|
+
*/
|
|
662
|
+
element.focus();
|
|
663
|
+
|
|
664
|
+
const range = document.createRange();
|
|
665
|
+
const selection = window.getSelection();
|
|
666
|
+
|
|
667
|
+
if (!selection) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Helper function to create a new text node and set the caret
|
|
673
|
+
* @param parent - parent element to append the text node
|
|
674
|
+
* @param prepend - should the text node be prepended or appended
|
|
675
|
+
*/
|
|
676
|
+
const createAndFocusTextNode = (parent: Node, prepend = false): void => {
|
|
677
|
+
const textNode = document.createTextNode('');
|
|
678
|
+
|
|
679
|
+
if (prepend) {
|
|
680
|
+
parent.insertBefore(textNode, parent.firstChild);
|
|
681
|
+
} else {
|
|
682
|
+
parent.appendChild(textNode);
|
|
683
|
+
}
|
|
684
|
+
range.setStart(textNode, 0);
|
|
685
|
+
range.setEnd(textNode, 0);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Find deepest text node in the given direction
|
|
690
|
+
* @param node - starting node
|
|
691
|
+
* @param toStart - search direction
|
|
692
|
+
*/
|
|
693
|
+
const findTextNode = (node: ChildNode | null, toStart: boolean): ChildNode | null => {
|
|
694
|
+
if (node === null) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
699
|
+
return node;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const nextChild = toStart ? node.firstChild : node.lastChild;
|
|
703
|
+
|
|
704
|
+
return findTextNode(nextChild, toStart);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* We need to set focus at start/end to the text node inside an element
|
|
709
|
+
*/
|
|
710
|
+
const childNodes = element.childNodes;
|
|
711
|
+
const initialNode: ChildNode | null = atStart ? childNodes[0] ?? null : childNodes[childNodes.length - 1] ?? null;
|
|
712
|
+
const nodeToFocus = findTextNode(initialNode, atStart);
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* If the element is empty, create a text node and place the caret at the start
|
|
716
|
+
*/
|
|
717
|
+
if (initialNode === null) {
|
|
718
|
+
createAndFocusTextNode(element);
|
|
719
|
+
selection.removeAllRanges();
|
|
720
|
+
selection.addRange(range);
|
|
721
|
+
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* If no text node is found, create one and set focus
|
|
727
|
+
*/
|
|
728
|
+
if (nodeToFocus === null || nodeToFocus.nodeType !== Node.TEXT_NODE) {
|
|
729
|
+
createAndFocusTextNode(element, atStart);
|
|
730
|
+
selection.removeAllRanges();
|
|
731
|
+
selection.addRange(range);
|
|
732
|
+
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* If a text node is found, place the caret
|
|
738
|
+
*/
|
|
739
|
+
const length = nodeToFocus.textContent?.length ?? 0;
|
|
740
|
+
const position = atStart ? 0 : length;
|
|
741
|
+
|
|
742
|
+
range.setStart(nodeToFocus, position);
|
|
743
|
+
range.setEnd(nodeToFocus, position);
|
|
744
|
+
|
|
745
|
+
selection.removeAllRanges();
|
|
746
|
+
selection.addRange(range);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Gets the current caret's X coordinate (horizontal position).
|
|
751
|
+
* Used for Notion-style navigation to preserve horizontal position when moving between blocks.
|
|
752
|
+
* @returns The X coordinate of the caret, or null if no selection exists
|
|
753
|
+
*/
|
|
754
|
+
export const getCaretXPosition = (): number | null => {
|
|
755
|
+
const selection = window.getSelection();
|
|
756
|
+
|
|
757
|
+
if (!selection || selection.rangeCount === 0) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const range = selection.getRangeAt(0);
|
|
762
|
+
const rect = range.getBoundingClientRect();
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* If the range has valid dimensions, return the left position
|
|
766
|
+
*/
|
|
767
|
+
const hasValidDimensions = rect.width !== 0 || rect.height !== 0 || rect.x !== 0;
|
|
768
|
+
|
|
769
|
+
if (hasValidDimensions) {
|
|
770
|
+
return rect.left;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* If the range has no dimensions (e.g., collapsed at start of empty element),
|
|
775
|
+
* try to get position from the container element
|
|
776
|
+
*/
|
|
777
|
+
const container = range.startContainer;
|
|
778
|
+
const element = container.nodeType === Node.ELEMENT_NODE
|
|
779
|
+
? container as HTMLElement
|
|
780
|
+
: container.parentElement;
|
|
781
|
+
|
|
782
|
+
if (!element) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const elementRect = element.getBoundingClientRect();
|
|
787
|
+
|
|
788
|
+
return elementRect.left;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Sets the caret position in an element at the closest position to the target X coordinate.
|
|
793
|
+
* This is used for Notion-style navigation to preserve horizontal position when moving between blocks.
|
|
794
|
+
* @param element - The contenteditable element or native input to set caret in
|
|
795
|
+
* @param targetX - The target X coordinate to match
|
|
796
|
+
* @param atFirstLine - If true, place caret on the first line; if false, place on the last line
|
|
797
|
+
*/
|
|
798
|
+
export const setCaretAtXPosition = (element: HTMLElement, targetX: number, atFirstLine: boolean): void => {
|
|
799
|
+
/**
|
|
800
|
+
* For native inputs, we need to find the character position that best matches the X coordinate
|
|
801
|
+
*/
|
|
802
|
+
if ($.isNativeInput(element)) {
|
|
803
|
+
setCaretAtXPositionInNativeInput(element as HTMLInputElement | HTMLTextAreaElement, targetX, atFirstLine);
|
|
804
|
+
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
setCaretAtXPositionInContentEditable(element, targetX, atFirstLine);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Sets caret position in a native input element at the closest position to target X
|
|
813
|
+
*/
|
|
814
|
+
const setCaretAtXPositionInNativeInput = (
|
|
815
|
+
input: HTMLInputElement | HTMLTextAreaElement,
|
|
816
|
+
targetX: number,
|
|
817
|
+
atFirstLine: boolean
|
|
818
|
+
): void => {
|
|
819
|
+
input.focus();
|
|
820
|
+
|
|
821
|
+
const value = input.value;
|
|
822
|
+
|
|
823
|
+
if (value.length === 0) {
|
|
824
|
+
input.setSelectionRange(0, 0);
|
|
825
|
+
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* For textareas with multiple lines, find the target line first
|
|
831
|
+
*/
|
|
832
|
+
if (input.tagName === 'TEXTAREA') {
|
|
833
|
+
const lines = value.split('\n');
|
|
834
|
+
const targetLineIndex = atFirstLine ? 0 : lines.length - 1;
|
|
835
|
+
const charOffset = lines.slice(0, targetLineIndex).reduce((acc, line) => acc + line.length + 1, 0);
|
|
836
|
+
|
|
837
|
+
const lineStart = charOffset;
|
|
838
|
+
const lineEnd = charOffset + lines[targetLineIndex].length;
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Binary search to find the best position within the line
|
|
842
|
+
*/
|
|
843
|
+
const bestPosition = findBestPositionInRange(input, lineStart, lineEnd, targetX);
|
|
844
|
+
|
|
845
|
+
input.setSelectionRange(bestPosition, bestPosition);
|
|
846
|
+
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* For single-line inputs, search the entire value
|
|
852
|
+
*/
|
|
853
|
+
const bestPosition = findBestPositionInRange(input, 0, value.length, targetX);
|
|
854
|
+
|
|
855
|
+
input.setSelectionRange(bestPosition, bestPosition);
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Binary search to find the character position closest to target X in a native input
|
|
860
|
+
*/
|
|
861
|
+
const findBestPositionInRange = (
|
|
862
|
+
input: HTMLInputElement | HTMLTextAreaElement,
|
|
863
|
+
start: number,
|
|
864
|
+
end: number,
|
|
865
|
+
targetX: number
|
|
866
|
+
): number => {
|
|
867
|
+
/**
|
|
868
|
+
* Create a temporary span to measure character positions
|
|
869
|
+
* This is a workaround since native inputs don't expose character positions directly
|
|
870
|
+
*/
|
|
871
|
+
const inputRect = input.getBoundingClientRect();
|
|
872
|
+
const style = window.getComputedStyle(input);
|
|
873
|
+
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* For native inputs, we approximate position based on character width
|
|
877
|
+
* This is not perfect but provides reasonable behavior
|
|
878
|
+
*/
|
|
879
|
+
const relativeX = targetX - inputRect.left - paddingLeft;
|
|
880
|
+
|
|
881
|
+
if (relativeX <= 0) {
|
|
882
|
+
return start;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Estimate character width and find approximate position
|
|
887
|
+
*/
|
|
888
|
+
const text = input.value.substring(start, end);
|
|
889
|
+
const fontSize = parseFloat(style.fontSize) || 16;
|
|
890
|
+
const avgCharWidth = fontSize * 0.6; // Approximate average character width
|
|
891
|
+
|
|
892
|
+
const estimatedPosition = Math.round(relativeX / avgCharWidth);
|
|
893
|
+
const clampedPosition = Math.min(Math.max(estimatedPosition, 0), text.length);
|
|
894
|
+
|
|
895
|
+
return start + clampedPosition;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Sets caret position in a contenteditable element at the closest position to target X
|
|
900
|
+
*/
|
|
901
|
+
const setCaretAtXPositionInContentEditable = (
|
|
902
|
+
element: HTMLElement,
|
|
903
|
+
targetX: number,
|
|
904
|
+
atFirstLine: boolean
|
|
905
|
+
): void => {
|
|
906
|
+
const selection = window.getSelection();
|
|
907
|
+
|
|
908
|
+
if (!selection) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Focus the element first to ensure it can receive a selection.
|
|
914
|
+
*/
|
|
915
|
+
element.focus();
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Get the target line's Y position
|
|
919
|
+
*/
|
|
920
|
+
const targetNode = atFirstLine
|
|
921
|
+
? $.getDeepestNode(element, false)
|
|
922
|
+
: $.getDeepestNode(element, true);
|
|
923
|
+
|
|
924
|
+
if (!targetNode) {
|
|
925
|
+
setSelectionToElement(element, selection, atFirstLine);
|
|
926
|
+
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Use document.caretPositionFromPoint or document.caretRangeFromPoint
|
|
932
|
+
* to find the position closest to the target X coordinate
|
|
933
|
+
*/
|
|
934
|
+
const targetY = getTargetYPosition(element, targetNode, atFirstLine);
|
|
935
|
+
|
|
936
|
+
if (targetY === null) {
|
|
937
|
+
setSelectionToElement(element, selection, atFirstLine);
|
|
938
|
+
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Try to use caretPositionFromPoint (standard) or caretRangeFromPoint (WebKit)
|
|
944
|
+
*/
|
|
945
|
+
const caretPosition = getCaretPositionFromPoint(targetX, targetY);
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Verify that the returned caret position is actually inside the target element.
|
|
949
|
+
* In Firefox, caretPositionFromPoint can return nodes outside the element
|
|
950
|
+
* (e.g., sibling elements like list markers) when the X coordinate is at the edge.
|
|
951
|
+
*/
|
|
952
|
+
if (caretPosition && element.contains(caretPosition.node)) {
|
|
953
|
+
const range = document.createRange();
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
range.setStart(caretPosition.node, caretPosition.offset);
|
|
957
|
+
range.setEnd(caretPosition.node, caretPosition.offset);
|
|
958
|
+
selection.removeAllRanges();
|
|
959
|
+
selection.addRange(range);
|
|
960
|
+
|
|
961
|
+
return;
|
|
962
|
+
} catch {
|
|
963
|
+
// Fall through to fallback
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Fallback: set selection to start or end of element
|
|
969
|
+
*/
|
|
970
|
+
setSelectionToElement(element, selection, atFirstLine);
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Sets selection to the start or end of an element.
|
|
975
|
+
* This is a cross-browser compatible way to position the caret.
|
|
976
|
+
*/
|
|
977
|
+
const setSelectionToElement = (
|
|
978
|
+
element: HTMLElement,
|
|
979
|
+
_selection: Selection,
|
|
980
|
+
atFirstLine: boolean
|
|
981
|
+
): void => {
|
|
982
|
+
/**
|
|
983
|
+
* Firefox and WebKit require the element to have focus before
|
|
984
|
+
* a selection can be set on it. We must also get a fresh Selection
|
|
985
|
+
* object AFTER focusing, as the pre-focus Selection may not work
|
|
986
|
+
* with the newly focused element in Firefox.
|
|
987
|
+
*/
|
|
988
|
+
element.focus();
|
|
989
|
+
|
|
990
|
+
const freshSelection = window.getSelection();
|
|
991
|
+
|
|
992
|
+
if (!freshSelection) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const targetNode = atFirstLine
|
|
997
|
+
? $.getDeepestNode(element, false)
|
|
998
|
+
: $.getDeepestNode(element, true);
|
|
999
|
+
|
|
1000
|
+
if (!targetNode) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const range = document.createRange();
|
|
1005
|
+
const offset = atFirstLine ? 0 : $.getContentLength(targetNode);
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
range.setStart(targetNode, offset);
|
|
1009
|
+
range.setEnd(targetNode, offset);
|
|
1010
|
+
freshSelection.removeAllRanges();
|
|
1011
|
+
freshSelection.addRange(range);
|
|
1012
|
+
} catch {
|
|
1013
|
+
// If setting range fails, use the focus utility which handles edge cases
|
|
1014
|
+
focus(element, atFirstLine);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Gets the Y coordinate for the target line (first or last)
|
|
1020
|
+
*/
|
|
1021
|
+
const getTargetYPosition = (element: HTMLElement, targetNode: Node, atFirstLine: boolean): number | null => {
|
|
1022
|
+
const range = document.createRange();
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
if (atFirstLine) {
|
|
1026
|
+
range.setStart(targetNode, 0);
|
|
1027
|
+
range.setEnd(targetNode, 0);
|
|
1028
|
+
} else {
|
|
1029
|
+
const length = $.getContentLength(targetNode);
|
|
1030
|
+
|
|
1031
|
+
range.setStart(targetNode, length);
|
|
1032
|
+
range.setEnd(targetNode, length);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const rect = range.getBoundingClientRect();
|
|
1036
|
+
|
|
1037
|
+
if (rect.height === 0 && rect.top === 0) {
|
|
1038
|
+
const elementRect = element.getBoundingClientRect();
|
|
1039
|
+
|
|
1040
|
+
return atFirstLine ? elementRect.top + 10 : elementRect.bottom - 10;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Return the vertical center of the line
|
|
1045
|
+
*/
|
|
1046
|
+
return rect.top + rect.height / 2;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Gets caret position from screen coordinates using browser APIs.
|
|
1054
|
+
* Uses the standard caretPositionFromPoint API which is now widely supported.
|
|
1055
|
+
*/
|
|
1056
|
+
const getCaretPositionFromPoint = (x: number, y: number): { node: Node; offset: number } | null => {
|
|
1057
|
+
const caretPosition = document.caretPositionFromPoint(x, y);
|
|
1058
|
+
|
|
1059
|
+
if (caretPosition === null) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
node: caretPosition.offsetNode,
|
|
1065
|
+
offset: caretPosition.offset,
|
|
1066
|
+
};
|
|
1067
|
+
};
|