@jackuait/blok 0.4.1-beta.5 → 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-Cl_5rkKS.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 +59 -156
- package/package.json +48 -16
- 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 +0 -5
- package/types/configs/blok-config.d.ts +21 -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 +0 -5
- package/dist/blok-DvN73wsH.mjs +0 -19922
- package/dist/blok.umd.js +0 -166
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextRange interface for IE9-
|
|
3
|
+
*/
|
|
4
|
+
import { log } from './utils';
|
|
5
|
+
import { Dom as $ } from './dom';
|
|
6
|
+
import { DATA_ATTR, createSelector } from './constants';
|
|
7
|
+
|
|
8
|
+
interface TextRange {
|
|
9
|
+
boundingTop: number;
|
|
10
|
+
boundingLeft: number;
|
|
11
|
+
boundingBottom: number;
|
|
12
|
+
boundingRight: number;
|
|
13
|
+
boundingHeight: number;
|
|
14
|
+
boundingWidth: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Interface for object returned by document.selection in IE9-
|
|
19
|
+
*/
|
|
20
|
+
interface MSSelection {
|
|
21
|
+
createRange: () => TextRange;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extends Document interface for IE9-
|
|
27
|
+
*/
|
|
28
|
+
interface Document {
|
|
29
|
+
selection?: MSSelection;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Working with selection
|
|
34
|
+
* @typedef {SelectionUtils} SelectionUtils
|
|
35
|
+
*/
|
|
36
|
+
export class SelectionUtils {
|
|
37
|
+
/**
|
|
38
|
+
* Selection instances
|
|
39
|
+
* @todo Check if this is still relevant
|
|
40
|
+
*/
|
|
41
|
+
public instance: Selection | null = null;
|
|
42
|
+
public selection: Selection | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This property can store SelectionUtils's range for restoring later
|
|
46
|
+
* @type {Range|null}
|
|
47
|
+
*/
|
|
48
|
+
public savedSelectionRange: Range | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fake background is active
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
public isFakeBackgroundEnabled = false;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The contenteditable element that had the selection when fake background was enabled
|
|
58
|
+
* Used to restore focus and selection when fake background is removed
|
|
59
|
+
*/
|
|
60
|
+
private selectionContainer: HTMLElement | null = null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns selected anchor
|
|
64
|
+
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
|
|
65
|
+
* @returns {Node|null}
|
|
66
|
+
*/
|
|
67
|
+
public static get anchorNode(): Node | null {
|
|
68
|
+
const selection = window.getSelection();
|
|
69
|
+
|
|
70
|
+
return selection ? selection.anchorNode : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns selected anchor element
|
|
75
|
+
* @returns {Element|null}
|
|
76
|
+
*/
|
|
77
|
+
public static get anchorElement(): Element | null {
|
|
78
|
+
const selection = window.getSelection();
|
|
79
|
+
|
|
80
|
+
if (!selection) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const anchorNode = selection.anchorNode;
|
|
85
|
+
|
|
86
|
+
if (!anchorNode) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!$.isElement(anchorNode)) {
|
|
91
|
+
return anchorNode.parentElement;
|
|
92
|
+
} else {
|
|
93
|
+
return anchorNode;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns selection offset according to the anchor node
|
|
99
|
+
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
|
|
100
|
+
* @returns {number|null}
|
|
101
|
+
*/
|
|
102
|
+
public static get anchorOffset(): number | null {
|
|
103
|
+
const selection = window.getSelection();
|
|
104
|
+
|
|
105
|
+
return selection ? selection.anchorOffset : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Is current selection range collapsed
|
|
110
|
+
* @returns {boolean|null}
|
|
111
|
+
*/
|
|
112
|
+
public static get isCollapsed(): boolean | null {
|
|
113
|
+
const selection = window.getSelection();
|
|
114
|
+
|
|
115
|
+
return selection ? selection.isCollapsed : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check current selection if it is at Blok's zone
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
public static get isAtBlok(): boolean {
|
|
123
|
+
return this.isSelectionAtBlok(SelectionUtils.get());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if passed selection is at Blok's zone
|
|
128
|
+
* @param selection - Selection object to check
|
|
129
|
+
*/
|
|
130
|
+
public static isSelectionAtBlok(selection: Selection | null): boolean {
|
|
131
|
+
if (!selection) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Something selected on document
|
|
137
|
+
*/
|
|
138
|
+
const initialNode = selection.anchorNode || selection.focusNode;
|
|
139
|
+
const selectedNode = initialNode && initialNode.nodeType === Node.TEXT_NODE
|
|
140
|
+
? initialNode.parentNode
|
|
141
|
+
: initialNode;
|
|
142
|
+
|
|
143
|
+
const blokZone = selectedNode && selectedNode instanceof Element
|
|
144
|
+
? selectedNode.closest(createSelector(DATA_ATTR.redactor))
|
|
145
|
+
: null;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* SelectionUtils is not out of Blok because Blok's wrapper was found
|
|
149
|
+
*/
|
|
150
|
+
return blokZone ? blokZone.nodeType === Node.ELEMENT_NODE : false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if passed range at Blok zone
|
|
155
|
+
* @param range - range to check
|
|
156
|
+
*/
|
|
157
|
+
public static isRangeAtBlok(range: Range): boolean | void {
|
|
158
|
+
if (!range) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const selectedNode: Node | null =
|
|
163
|
+
range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE
|
|
164
|
+
? range.startContainer.parentNode
|
|
165
|
+
: range.startContainer;
|
|
166
|
+
|
|
167
|
+
const blokZone =
|
|
168
|
+
selectedNode && selectedNode instanceof Element
|
|
169
|
+
? selectedNode.closest(createSelector(DATA_ATTR.redactor))
|
|
170
|
+
: null;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* SelectionUtils is not out of Blok because Blok's wrapper was found
|
|
174
|
+
*/
|
|
175
|
+
return blokZone ? blokZone.nodeType === Node.ELEMENT_NODE : false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Methods return boolean that true if selection exists on the page
|
|
180
|
+
*/
|
|
181
|
+
public static get isSelectionExists(): boolean {
|
|
182
|
+
const selection = SelectionUtils.get();
|
|
183
|
+
|
|
184
|
+
return !!selection?.anchorNode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Return first range
|
|
189
|
+
* @returns {Range|null}
|
|
190
|
+
*/
|
|
191
|
+
public static get range(): Range | null {
|
|
192
|
+
return this.getRangeFromSelection(this.get());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns range from passed Selection object
|
|
197
|
+
* @param selection - Selection object to get Range from
|
|
198
|
+
*/
|
|
199
|
+
public static getRangeFromSelection(selection: Selection | null): Range | null {
|
|
200
|
+
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Calculates position and size of selected text
|
|
205
|
+
* @returns {DOMRect}
|
|
206
|
+
*/
|
|
207
|
+
public static get rect(): DOMRect {
|
|
208
|
+
const ieSel: Selection | MSSelection | undefined | null = (document as Document).selection;
|
|
209
|
+
|
|
210
|
+
const rect = {
|
|
211
|
+
x: 0,
|
|
212
|
+
y: 0,
|
|
213
|
+
width: 0,
|
|
214
|
+
height: 0,
|
|
215
|
+
} as DOMRect;
|
|
216
|
+
|
|
217
|
+
if (ieSel && ieSel.type !== 'Control') {
|
|
218
|
+
const msSel = ieSel as MSSelection;
|
|
219
|
+
const range = msSel.createRange() as TextRange;
|
|
220
|
+
|
|
221
|
+
rect.x = range.boundingLeft;
|
|
222
|
+
rect.y = range.boundingTop;
|
|
223
|
+
rect.width = range.boundingWidth;
|
|
224
|
+
rect.height = range.boundingHeight;
|
|
225
|
+
|
|
226
|
+
return rect;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const sel = window.getSelection();
|
|
230
|
+
|
|
231
|
+
if (!sel) {
|
|
232
|
+
log('Method window.getSelection returned null', 'warn');
|
|
233
|
+
|
|
234
|
+
return rect;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (sel.rangeCount === null || isNaN(sel.rangeCount)) {
|
|
238
|
+
log('Method SelectionUtils.rangeCount is not supported', 'warn');
|
|
239
|
+
|
|
240
|
+
return rect;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (sel.rangeCount === 0) {
|
|
244
|
+
return rect;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const range = sel.getRangeAt(0).cloneRange() as Range;
|
|
248
|
+
|
|
249
|
+
const initialRect = range.getBoundingClientRect() as DOMRect;
|
|
250
|
+
|
|
251
|
+
// Fall back to inserting a temporary element
|
|
252
|
+
if (initialRect.x === 0 && initialRect.y === 0) {
|
|
253
|
+
const span = document.createElement('span');
|
|
254
|
+
|
|
255
|
+
// Ensure span has dimensions and position by
|
|
256
|
+
// adding a zero-width space character
|
|
257
|
+
span.appendChild(document.createTextNode('\u200b'));
|
|
258
|
+
range.insertNode(span);
|
|
259
|
+
const boundingRect = span.getBoundingClientRect() as DOMRect;
|
|
260
|
+
|
|
261
|
+
const spanParent = span.parentNode;
|
|
262
|
+
|
|
263
|
+
spanParent?.removeChild(span);
|
|
264
|
+
|
|
265
|
+
// Glue any broken text nodes back together
|
|
266
|
+
spanParent?.normalize();
|
|
267
|
+
|
|
268
|
+
return boundingRect;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return initialRect;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Returns selected text as String
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
public static get text(): string {
|
|
279
|
+
const selection = window.getSelection();
|
|
280
|
+
|
|
281
|
+
return selection?.toString() ?? '';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Returns window SelectionUtils
|
|
286
|
+
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
|
|
287
|
+
* @returns {Selection}
|
|
288
|
+
*/
|
|
289
|
+
public static get(): Selection | null {
|
|
290
|
+
return window.getSelection();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Set focus to contenteditable or native input element
|
|
295
|
+
* @param element - element where to set focus
|
|
296
|
+
* @param offset - offset of cursor
|
|
297
|
+
*/
|
|
298
|
+
public static setCursor(element: HTMLElement, offset = 0): DOMRect {
|
|
299
|
+
const range = document.createRange();
|
|
300
|
+
const selection = window.getSelection();
|
|
301
|
+
|
|
302
|
+
const isNativeInput = $.isNativeInput(element);
|
|
303
|
+
|
|
304
|
+
/** if found deepest node is native input */
|
|
305
|
+
if (isNativeInput && !$.canSetCaret(element)) {
|
|
306
|
+
return element.getBoundingClientRect();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isNativeInput) {
|
|
310
|
+
const inputElement = element as HTMLInputElement | HTMLTextAreaElement;
|
|
311
|
+
|
|
312
|
+
inputElement.focus();
|
|
313
|
+
inputElement.selectionStart = offset;
|
|
314
|
+
inputElement.selectionEnd = offset;
|
|
315
|
+
|
|
316
|
+
return inputElement.getBoundingClientRect();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
range.setStart(element, offset);
|
|
320
|
+
range.setEnd(element, offset);
|
|
321
|
+
|
|
322
|
+
if (!selection) {
|
|
323
|
+
return element.getBoundingClientRect();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
selection.removeAllRanges();
|
|
327
|
+
selection.addRange(range);
|
|
328
|
+
|
|
329
|
+
return range.getBoundingClientRect();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if current range exists and belongs to container
|
|
334
|
+
* @param container - where range should be
|
|
335
|
+
*/
|
|
336
|
+
public static isRangeInsideContainer(container: HTMLElement): boolean {
|
|
337
|
+
const range = SelectionUtils.range;
|
|
338
|
+
|
|
339
|
+
if (range === null) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return container.contains(range.startContainer);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Adds fake cursor to the current range
|
|
348
|
+
*/
|
|
349
|
+
public static addFakeCursor(): void {
|
|
350
|
+
const range = SelectionUtils.range;
|
|
351
|
+
|
|
352
|
+
if (range === null) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const fakeCursor = $.make('span');
|
|
357
|
+
|
|
358
|
+
fakeCursor.setAttribute(DATA_ATTR.fakeCursor, '');
|
|
359
|
+
fakeCursor.setAttribute('data-blok-mutation-free', 'true');
|
|
360
|
+
|
|
361
|
+
range.collapse();
|
|
362
|
+
range.insertNode(fakeCursor);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if passed element contains a fake cursor
|
|
367
|
+
* @param el - where to check
|
|
368
|
+
*/
|
|
369
|
+
public static isFakeCursorInsideContainer(el: HTMLElement): boolean {
|
|
370
|
+
return $.find(el, createSelector(DATA_ATTR.fakeCursor)) !== null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Removes fake cursor from a container
|
|
375
|
+
* @param container - container to look for
|
|
376
|
+
*/
|
|
377
|
+
public static removeFakeCursor(container: HTMLElement = document.body): void {
|
|
378
|
+
const fakeCursor = $.find(container, createSelector(DATA_ATTR.fakeCursor));
|
|
379
|
+
|
|
380
|
+
if (!fakeCursor) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fakeCursor.remove();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Removes fake background
|
|
389
|
+
* Unwraps the highlight spans and restores the selection
|
|
390
|
+
*/
|
|
391
|
+
public removeFakeBackground(): void {
|
|
392
|
+
// Always clean up any orphaned fake background elements in the DOM
|
|
393
|
+
// This handles cleanup after undo/redo operations that may restore fake background elements
|
|
394
|
+
this.removeOrphanedFakeBackgroundElements();
|
|
395
|
+
|
|
396
|
+
if (!this.isFakeBackgroundEnabled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Remove the highlight spans
|
|
401
|
+
this.removeHighlightSpans();
|
|
402
|
+
|
|
403
|
+
this.isFakeBackgroundEnabled = false;
|
|
404
|
+
this.selectionContainer = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Removes highlight spans and reconstructs the saved selection range
|
|
409
|
+
*/
|
|
410
|
+
private removeHighlightSpans(): void {
|
|
411
|
+
const highlightSpans = document.querySelectorAll('[data-blok-fake-background="true"]');
|
|
412
|
+
|
|
413
|
+
if (highlightSpans.length === 0) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const firstSpan = highlightSpans[0] as HTMLElement;
|
|
418
|
+
const lastSpan = highlightSpans[highlightSpans.length - 1] as HTMLElement;
|
|
419
|
+
|
|
420
|
+
const firstChild = firstSpan.firstChild;
|
|
421
|
+
const lastChild = lastSpan.lastChild;
|
|
422
|
+
|
|
423
|
+
highlightSpans.forEach((element) => {
|
|
424
|
+
this.unwrapFakeBackground(element as HTMLElement);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Reconstruct the selection range after unwrapping
|
|
428
|
+
if (firstChild && lastChild) {
|
|
429
|
+
const newRange = document.createRange();
|
|
430
|
+
|
|
431
|
+
newRange.setStart(firstChild, 0);
|
|
432
|
+
newRange.setEnd(lastChild, lastChild.textContent?.length || 0);
|
|
433
|
+
this.savedSelectionRange = newRange;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Removes any fake background elements from the DOM that are not tracked
|
|
439
|
+
* This handles cleanup after undo/redo operations that may restore fake background elements
|
|
440
|
+
* Also provides backwards compatibility with old fake background approach
|
|
441
|
+
*/
|
|
442
|
+
public removeOrphanedFakeBackgroundElements(): void {
|
|
443
|
+
const orphanedElements = document.querySelectorAll('[data-blok-fake-background="true"]');
|
|
444
|
+
|
|
445
|
+
orphanedElements.forEach((element) => {
|
|
446
|
+
this.unwrapFakeBackground(element as HTMLElement);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Clears all fake background state - both DOM elements and internal flags
|
|
452
|
+
* This is useful for cleanup after undo/redo operations or when the selection context has been lost
|
|
453
|
+
*/
|
|
454
|
+
public clearFakeBackground(): void {
|
|
455
|
+
this.removeOrphanedFakeBackgroundElements();
|
|
456
|
+
this.isFakeBackgroundEnabled = false;
|
|
457
|
+
this.selectionContainer = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Sets fake background by wrapping selected text in highlight spans
|
|
462
|
+
* Uses a gray background color to simulate the "unfocused selection" appearance
|
|
463
|
+
* similar to how Notion shows selections when focus moves to another element
|
|
464
|
+
*/
|
|
465
|
+
public setFakeBackground(): void {
|
|
466
|
+
this.removeFakeBackground();
|
|
467
|
+
|
|
468
|
+
const selection = window.getSelection();
|
|
469
|
+
|
|
470
|
+
if (!selection || selection.rangeCount === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const range = selection.getRangeAt(0);
|
|
475
|
+
|
|
476
|
+
if (range.collapsed) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Find the contenteditable container that holds the selection
|
|
481
|
+
const container = range.commonAncestorContainer;
|
|
482
|
+
const element = container.nodeType === Node.ELEMENT_NODE
|
|
483
|
+
? container as HTMLElement
|
|
484
|
+
: container.parentElement;
|
|
485
|
+
|
|
486
|
+
this.selectionContainer = element?.closest('[contenteditable="true"]') as HTMLElement | null;
|
|
487
|
+
|
|
488
|
+
// Collect text nodes and wrap them with highlight spans
|
|
489
|
+
const textNodes = this.collectTextNodes(range);
|
|
490
|
+
|
|
491
|
+
if (textNodes.length === 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const anchorStartNode = range.startContainer;
|
|
496
|
+
const anchorStartOffset = range.startOffset;
|
|
497
|
+
const anchorEndNode = range.endContainer;
|
|
498
|
+
const anchorEndOffset = range.endOffset;
|
|
499
|
+
|
|
500
|
+
const highlightSpans: HTMLElement[] = [];
|
|
501
|
+
|
|
502
|
+
textNodes.forEach((textNode) => {
|
|
503
|
+
const segmentRange = document.createRange();
|
|
504
|
+
const isStartNode = textNode === anchorStartNode;
|
|
505
|
+
const isEndNode = textNode === anchorEndNode;
|
|
506
|
+
const startOffset = isStartNode ? anchorStartOffset : 0;
|
|
507
|
+
const nodeTextLength = textNode.textContent?.length ?? 0;
|
|
508
|
+
const endOffset = isEndNode ? anchorEndOffset : nodeTextLength;
|
|
509
|
+
|
|
510
|
+
if (startOffset === endOffset) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
segmentRange.setStart(textNode, startOffset);
|
|
515
|
+
segmentRange.setEnd(textNode, endOffset);
|
|
516
|
+
|
|
517
|
+
const wrapper = this.wrapRangeWithHighlight(segmentRange);
|
|
518
|
+
|
|
519
|
+
if (wrapper) {
|
|
520
|
+
highlightSpans.push(wrapper);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (highlightSpans.length === 0) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Post-process: split multi-line spans and apply box-shadow styling
|
|
529
|
+
const processedSpans = this.postProcessHighlightWrappers(highlightSpans);
|
|
530
|
+
|
|
531
|
+
// Apply additional line-height extensions for gaps between separate spans
|
|
532
|
+
this.applyLineHeightExtensions(processedSpans);
|
|
533
|
+
|
|
534
|
+
// Create a visual range spanning all highlight spans
|
|
535
|
+
const visualRange = document.createRange();
|
|
536
|
+
|
|
537
|
+
visualRange.setStartBefore(processedSpans[0]);
|
|
538
|
+
visualRange.setEndAfter(processedSpans[processedSpans.length - 1]);
|
|
539
|
+
|
|
540
|
+
// Save the range for later restoration
|
|
541
|
+
this.savedSelectionRange = visualRange.cloneRange();
|
|
542
|
+
|
|
543
|
+
// Update the browser selection to span the fake background elements
|
|
544
|
+
// Re-get selection in case it was cleared earlier
|
|
545
|
+
const currentSelection = window.getSelection();
|
|
546
|
+
|
|
547
|
+
if (currentSelection) {
|
|
548
|
+
currentSelection.removeAllRanges();
|
|
549
|
+
currentSelection.addRange(visualRange);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.isFakeBackgroundEnabled = true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Collects text nodes that intersect with the passed range
|
|
557
|
+
* @param range - selection range
|
|
558
|
+
*/
|
|
559
|
+
private collectTextNodes(range: Range): Text[] {
|
|
560
|
+
const nodes: Text[] = [];
|
|
561
|
+
const { commonAncestorContainer } = range;
|
|
562
|
+
|
|
563
|
+
if (commonAncestorContainer.nodeType === Node.TEXT_NODE) {
|
|
564
|
+
nodes.push(commonAncestorContainer as Text);
|
|
565
|
+
|
|
566
|
+
return nodes;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const walker = document.createTreeWalker(
|
|
570
|
+
commonAncestorContainer,
|
|
571
|
+
NodeFilter.SHOW_TEXT,
|
|
572
|
+
{
|
|
573
|
+
acceptNode: (node: Node): number => {
|
|
574
|
+
if (!range.intersectsNode(node)) {
|
|
575
|
+
return NodeFilter.FILTER_REJECT;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return node.textContent && node.textContent.length > 0
|
|
579
|
+
? NodeFilter.FILTER_ACCEPT
|
|
580
|
+
: NodeFilter.FILTER_REJECT;
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
while (walker.nextNode()) {
|
|
586
|
+
nodes.push(walker.currentNode as Text);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return nodes;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Wraps passed range with a highlight span styled like an unfocused selection (gray)
|
|
594
|
+
* @param range - range to wrap
|
|
595
|
+
*/
|
|
596
|
+
private wrapRangeWithHighlight(range: Range): HTMLElement | null {
|
|
597
|
+
if (range.collapsed) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const wrapper = $.make('span');
|
|
602
|
+
|
|
603
|
+
wrapper.setAttribute('data-blok-testid', 'fake-background');
|
|
604
|
+
wrapper.setAttribute('data-blok-fake-background', 'true');
|
|
605
|
+
wrapper.setAttribute('data-blok-mutation-free', 'true');
|
|
606
|
+
// Don't use background-color here - we'll use box-shadow only to avoid overlap issues
|
|
607
|
+
// The box-shadow will be applied later in applyLineHeightExtensions
|
|
608
|
+
wrapper.style.color = 'inherit';
|
|
609
|
+
// box-decoration-break: clone ensures background/padding applies per-line for multi-line inline elements
|
|
610
|
+
wrapper.style.boxDecorationBreak = 'clone';
|
|
611
|
+
(wrapper.style as unknown as Record<string, string>)['-webkit-box-decoration-break'] = 'clone';
|
|
612
|
+
// Preserve trailing whitespace so the highlight covers spaces at end of lines
|
|
613
|
+
wrapper.style.whiteSpace = 'pre-wrap';
|
|
614
|
+
|
|
615
|
+
const contents = range.extractContents();
|
|
616
|
+
|
|
617
|
+
if (contents.childNodes.length === 0) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
wrapper.appendChild(contents);
|
|
622
|
+
range.insertNode(wrapper);
|
|
623
|
+
|
|
624
|
+
return wrapper;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Post-processes highlight wrappers to split multi-line spans and apply proper styling
|
|
629
|
+
* @param wrappers - array of wrapper elements
|
|
630
|
+
* @returns array of all wrapper elements (may be more than input if splits occurred)
|
|
631
|
+
*/
|
|
632
|
+
private postProcessHighlightWrappers(wrappers: HTMLElement[]): HTMLElement[] {
|
|
633
|
+
const allWrappers: HTMLElement[] = [];
|
|
634
|
+
|
|
635
|
+
wrappers.forEach((wrapper) => {
|
|
636
|
+
const splitWrappers = this.splitMultiLineWrapper(wrapper);
|
|
637
|
+
|
|
638
|
+
allWrappers.push(...splitWrappers);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return allWrappers;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Splits a multi-line wrapper into separate spans per line and applies box-shadow to each
|
|
646
|
+
* This ensures gaps between lines are properly filled
|
|
647
|
+
* @param wrapper - the highlight wrapper element
|
|
648
|
+
* @returns array of wrapper elements (original if single line, or new per-line wrappers)
|
|
649
|
+
*/
|
|
650
|
+
private splitMultiLineWrapper(wrapper: HTMLElement): HTMLElement[] {
|
|
651
|
+
const clientRects = wrapper.getClientRects();
|
|
652
|
+
|
|
653
|
+
// If single line, just apply box-shadow and return
|
|
654
|
+
if (clientRects.length <= 1) {
|
|
655
|
+
this.applyBoxShadowToWrapper(wrapper);
|
|
656
|
+
|
|
657
|
+
return [wrapper];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Multi-line: we need to split the text into separate spans per line
|
|
661
|
+
// This is done by using Range to find line breaks
|
|
662
|
+
const textContent = wrapper.textContent || '';
|
|
663
|
+
const parent = wrapper.parentNode;
|
|
664
|
+
|
|
665
|
+
if (!parent || !textContent) {
|
|
666
|
+
this.applyBoxShadowToWrapper(wrapper);
|
|
667
|
+
|
|
668
|
+
return [wrapper];
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Create a temporary range to measure character positions
|
|
672
|
+
const wrappers: HTMLElement[] = [];
|
|
673
|
+
const textNode = wrapper.firstChild;
|
|
674
|
+
|
|
675
|
+
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
|
|
676
|
+
this.applyBoxShadowToWrapper(wrapper);
|
|
677
|
+
|
|
678
|
+
return [wrapper];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Find line break positions by checking character rects
|
|
682
|
+
const lineBreaks = this.findLineBreakPositions(textNode as Text, clientRects.length);
|
|
683
|
+
|
|
684
|
+
if (lineBreaks.length === 0) {
|
|
685
|
+
this.applyBoxShadowToWrapper(wrapper);
|
|
686
|
+
|
|
687
|
+
return [wrapper];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Split the text at line breaks and create new wrappers
|
|
691
|
+
const segments = this.splitTextAtPositions(textContent, lineBreaks);
|
|
692
|
+
|
|
693
|
+
// Replace the original wrapper with multiple wrappers
|
|
694
|
+
const fragment = document.createDocumentFragment();
|
|
695
|
+
|
|
696
|
+
segments.forEach((segment) => {
|
|
697
|
+
if (segment.length === 0) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const newWrapper = $.make('span');
|
|
702
|
+
|
|
703
|
+
newWrapper.setAttribute('data-blok-testid', 'fake-background');
|
|
704
|
+
newWrapper.setAttribute('data-blok-fake-background', 'true');
|
|
705
|
+
newWrapper.setAttribute('data-blok-mutation-free', 'true');
|
|
706
|
+
// Don't use background-color - box-shadow will be applied later
|
|
707
|
+
newWrapper.style.color = 'inherit';
|
|
708
|
+
newWrapper.style.boxDecorationBreak = 'clone';
|
|
709
|
+
(newWrapper.style as unknown as Record<string, string>)['-webkit-box-decoration-break'] = 'clone';
|
|
710
|
+
// Preserve trailing whitespace so the highlight covers spaces at end of lines
|
|
711
|
+
newWrapper.style.whiteSpace = 'pre-wrap';
|
|
712
|
+
newWrapper.textContent = segment;
|
|
713
|
+
|
|
714
|
+
fragment.appendChild(newWrapper);
|
|
715
|
+
wrappers.push(newWrapper);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
parent.replaceChild(fragment, wrapper);
|
|
719
|
+
|
|
720
|
+
return wrappers;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Splits text content at given positions
|
|
725
|
+
*/
|
|
726
|
+
private splitTextAtPositions(text: string, positions: number[]): string[] {
|
|
727
|
+
const breakPoints = [0, ...positions, text.length];
|
|
728
|
+
|
|
729
|
+
return breakPoints.slice(0, -1).map((start, idx) => {
|
|
730
|
+
return text.substring(start, breakPoints[idx + 1]);
|
|
731
|
+
}).filter((segment) => segment.length > 0);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Finds positions in text where line breaks occur
|
|
736
|
+
* @param textNode - the text node to analyze
|
|
737
|
+
* @param expectedLines - expected number of lines
|
|
738
|
+
*/
|
|
739
|
+
private findLineBreakPositions(textNode: Text, expectedLines: number): number[] {
|
|
740
|
+
const text = textNode.textContent || '';
|
|
741
|
+
const range = document.createRange();
|
|
742
|
+
const indices = Array.from({ length: text.length }, (_, i) => i);
|
|
743
|
+
|
|
744
|
+
const result = indices.reduce(
|
|
745
|
+
(acc: { positions: number[]; lastTop: number }, i: number) => {
|
|
746
|
+
if (acc.positions.length >= expectedLines - 1) {
|
|
747
|
+
return acc;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
range.setStart(textNode, i);
|
|
751
|
+
range.setEnd(textNode, i + 1);
|
|
752
|
+
|
|
753
|
+
const rect = range.getBoundingClientRect();
|
|
754
|
+
const isLineBreak = acc.lastTop !== -1 && Math.abs(rect.top - acc.lastTop) > 5;
|
|
755
|
+
|
|
756
|
+
if (isLineBreak) {
|
|
757
|
+
acc.positions.push(i);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return { positions: acc.positions, lastTop: rect.top };
|
|
761
|
+
},
|
|
762
|
+
{ positions: [], lastTop: -1 }
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
return result.positions;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Applies box-shadow to a wrapper to extend the background to fill line-height
|
|
770
|
+
* @param wrapper - the wrapper element
|
|
771
|
+
*/
|
|
772
|
+
private applyBoxShadowToWrapper(wrapper: HTMLElement): void {
|
|
773
|
+
const parent = wrapper.parentElement;
|
|
774
|
+
|
|
775
|
+
if (!parent) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
780
|
+
const wrapperStyle = window.getComputedStyle(wrapper);
|
|
781
|
+
|
|
782
|
+
const lineHeight = parseFloat(parentStyle.lineHeight);
|
|
783
|
+
const fontSize = parseFloat(wrapperStyle.fontSize);
|
|
784
|
+
|
|
785
|
+
// If lineHeight is NaN (e.g., "normal"), estimate it as 1.2 * fontSize
|
|
786
|
+
const effectiveLineHeight = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight;
|
|
787
|
+
|
|
788
|
+
// Calculate extension needed to fill the line-height
|
|
789
|
+
const rect = wrapper.getBoundingClientRect();
|
|
790
|
+
const extension = Math.max(0, (effectiveLineHeight - rect.height) / 2);
|
|
791
|
+
|
|
792
|
+
if (extension > 0) {
|
|
793
|
+
const bgColor = 'rgba(0, 0, 0, 0.08)';
|
|
794
|
+
|
|
795
|
+
// eslint-disable-next-line no-param-reassign
|
|
796
|
+
wrapper.style.boxShadow = `0 ${extension}px 0 ${bgColor}, 0 -${extension}px 0 ${bgColor}`;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Applies additional box-shadow extensions to fill gaps between separate spans
|
|
802
|
+
* This is only needed when there are multiple spans that may have gaps between them
|
|
803
|
+
* @param spans - array of highlight span elements
|
|
804
|
+
*/
|
|
805
|
+
private applyLineHeightExtensions(spans: HTMLElement[]): void {
|
|
806
|
+
|
|
807
|
+
const bgColor = 'rgba(0, 0, 0, 0.08)';
|
|
808
|
+
|
|
809
|
+
// Collect all line rects from all spans
|
|
810
|
+
const allLineRects = this.collectAllLineRects(spans);
|
|
811
|
+
|
|
812
|
+
if (allLineRects.length === 0) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Sort by vertical position
|
|
817
|
+
allLineRects.sort((a, b) => a.top - b.top);
|
|
818
|
+
|
|
819
|
+
// Group rects that are on the same visual line
|
|
820
|
+
const lineGroups = this.groupRectsByLine(allLineRects);
|
|
821
|
+
|
|
822
|
+
// Apply box-shadow to each span based on its line position (for inter-span gaps)
|
|
823
|
+
spans.forEach((span) => {
|
|
824
|
+
this.applyMultiLineBoxShadow(span, lineGroups, bgColor);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Collects all line rectangles from all spans using getClientRects()
|
|
830
|
+
*/
|
|
831
|
+
private collectAllLineRects(spans: HTMLElement[]): Array<{ top: number; bottom: number; span: HTMLElement }> {
|
|
832
|
+
const rects: Array<{ top: number; bottom: number; span: HTMLElement }> = [];
|
|
833
|
+
|
|
834
|
+
spans.forEach((span) => {
|
|
835
|
+
const clientRects = span.getClientRects();
|
|
836
|
+
|
|
837
|
+
Array.from(clientRects).forEach((rect) => {
|
|
838
|
+
rects.push({
|
|
839
|
+
top: rect.top,
|
|
840
|
+
bottom: rect.bottom,
|
|
841
|
+
span,
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
return rects;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Groups rectangles by their visual line
|
|
851
|
+
*/
|
|
852
|
+
private groupRectsByLine(
|
|
853
|
+
rects: Array<{ top: number; bottom: number; span: HTMLElement }>
|
|
854
|
+
): Array<{ top: number; bottom: number }> {
|
|
855
|
+
const lines: Array<{ top: number; bottom: number }> = [];
|
|
856
|
+
|
|
857
|
+
rects.forEach((rect) => {
|
|
858
|
+
// Find if this rect belongs to an existing line
|
|
859
|
+
const existingLine = lines.find((line) => Math.abs(line.top - rect.top) < 2);
|
|
860
|
+
|
|
861
|
+
if (existingLine) {
|
|
862
|
+
// Extend the line if needed
|
|
863
|
+
existingLine.top = Math.min(existingLine.top, rect.top);
|
|
864
|
+
existingLine.bottom = Math.max(existingLine.bottom, rect.bottom);
|
|
865
|
+
} else {
|
|
866
|
+
lines.push({ top: rect.top, bottom: rect.bottom });
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Sort lines by top position
|
|
871
|
+
lines.sort((a, b) => a.top - b.top);
|
|
872
|
+
|
|
873
|
+
return lines;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Applies box-shadow to a span that may span multiple lines
|
|
878
|
+
* Calculates extensions based on the span's position within the overall selection
|
|
879
|
+
*/
|
|
880
|
+
private applyMultiLineBoxShadow(
|
|
881
|
+
span: HTMLElement,
|
|
882
|
+
lineGroups: Array<{ top: number; bottom: number }>,
|
|
883
|
+
bgColor: string
|
|
884
|
+
): void {
|
|
885
|
+
const clientRects = span.getClientRects();
|
|
886
|
+
|
|
887
|
+
if (clientRects.length === 0) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const parent = span.parentElement;
|
|
892
|
+
|
|
893
|
+
if (!parent) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Calculate base extension from line-height
|
|
898
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
899
|
+
const lineHeight = parseFloat(parentStyle.lineHeight);
|
|
900
|
+
const fontSize = parseFloat(window.getComputedStyle(span).fontSize);
|
|
901
|
+
const effectiveLineHeight = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight;
|
|
902
|
+
|
|
903
|
+
// Get first and last rects (same for single-line, different for multi-line)
|
|
904
|
+
const firstRect = clientRects[0];
|
|
905
|
+
const lastRect = clientRects[clientRects.length - 1];
|
|
906
|
+
|
|
907
|
+
const firstLineIndex = this.findLineIndex(firstRect.top, lineGroups);
|
|
908
|
+
const lastLineIndex = this.findLineIndex(lastRect.top, lineGroups);
|
|
909
|
+
|
|
910
|
+
// Check if this span itself spans multiple lines (not just part of a multi-line selection)
|
|
911
|
+
const spanSpansMultipleLines = clientRects.length > 1 && firstLineIndex !== lastLineIndex;
|
|
912
|
+
|
|
913
|
+
const isFirstLine = firstLineIndex === 0;
|
|
914
|
+
const isLastLine = lastLineIndex === lineGroups.length - 1;
|
|
915
|
+
|
|
916
|
+
// Calculate extension based on line-height
|
|
917
|
+
const baseExtension = Math.max(0, (effectiveLineHeight - firstRect.height) / 2);
|
|
918
|
+
|
|
919
|
+
// Only apply gap-filling logic if this span itself spans multiple lines
|
|
920
|
+
// For single-line spans, just use base extension for both top and bottom
|
|
921
|
+
const topExtension = spanSpansMultipleLines
|
|
922
|
+
? this.calculateLineTopExtension(baseExtension, isFirstLine, lineGroups, firstLineIndex)
|
|
923
|
+
: baseExtension;
|
|
924
|
+
const bottomExtension = spanSpansMultipleLines
|
|
925
|
+
? this.calculateLineBottomExtension(baseExtension, isLastLine, lineGroups, lastLineIndex)
|
|
926
|
+
: baseExtension;
|
|
927
|
+
|
|
928
|
+
const boxShadow = this.buildBoxShadow(topExtension, bottomExtension, bgColor);
|
|
929
|
+
|
|
930
|
+
// eslint-disable-next-line no-param-reassign
|
|
931
|
+
span.style.boxShadow = boxShadow;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Finds the line index for a given top position
|
|
936
|
+
*/
|
|
937
|
+
private findLineIndex(top: number, lineGroups: Array<{ top: number; bottom: number }>): number {
|
|
938
|
+
const index = lineGroups.findIndex((line) => Math.abs(line.top - top) < 5);
|
|
939
|
+
|
|
940
|
+
return index >= 0 ? index : 0;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Calculates top extension for a line
|
|
945
|
+
* Only uses base extension - gaps are filled by the previous line's bottom extension
|
|
946
|
+
*/
|
|
947
|
+
private calculateLineTopExtension(
|
|
948
|
+
baseExtension: number,
|
|
949
|
+
_isFirstLine: boolean,
|
|
950
|
+
_lineGroups: Array<{ top: number; bottom: number }>,
|
|
951
|
+
_lineIndex: number
|
|
952
|
+
): number {
|
|
953
|
+
// Top extension is always just the base extension
|
|
954
|
+
// The gap between lines is filled entirely by the previous line's bottom extension
|
|
955
|
+
// This prevents overlapping shadows that would cause darker bands
|
|
956
|
+
return baseExtension;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Calculates bottom extension for a line, accounting for gap to next line
|
|
961
|
+
* The bottom extension fills the gap up to where the next line's top extension begins
|
|
962
|
+
* This prevents overlap: line N's bottom shadow meets line N+1's top shadow exactly
|
|
963
|
+
*/
|
|
964
|
+
private calculateLineBottomExtension(
|
|
965
|
+
baseExtension: number,
|
|
966
|
+
isLastLine: boolean,
|
|
967
|
+
lineGroups: Array<{ top: number; bottom: number }>,
|
|
968
|
+
lineIndex: number
|
|
969
|
+
): number {
|
|
970
|
+
if (isLastLine) {
|
|
971
|
+
return baseExtension;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const currentLine = lineGroups[lineIndex];
|
|
975
|
+
const nextLine = lineGroups[lineIndex + 1];
|
|
976
|
+
|
|
977
|
+
// The next line's span will have its own top extension (baseExtension)
|
|
978
|
+
// So we only need to extend to meet that point, not overlap it
|
|
979
|
+
// Gap = nextLine.top - currentLine.bottom
|
|
980
|
+
// Next line's top extension covers: nextLine.top - baseExtension to nextLine.top
|
|
981
|
+
// So we extend from currentLine.bottom to (nextLine.top - baseExtension)
|
|
982
|
+
const gapToNextLine = nextLine.top - currentLine.bottom;
|
|
983
|
+
const nextLineTopExtension = baseExtension; // Next line will also extend up by baseExtension
|
|
984
|
+
|
|
985
|
+
// We extend: baseExtension (our own) + gap - nextLineTopExtension
|
|
986
|
+
// This way: our bottom = currentLine.bottom + baseExtension + gap - baseExtension
|
|
987
|
+
// = currentLine.bottom + gap = nextLine.top - baseExtension + baseExtension...
|
|
988
|
+
// Actually simpler: extend to fill gap minus what next line covers
|
|
989
|
+
const gapWeNeedToCover = Math.max(0, gapToNextLine - nextLineTopExtension);
|
|
990
|
+
|
|
991
|
+
return baseExtension + gapWeNeedToCover;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Builds box-shadow CSS value from top and bottom extensions
|
|
998
|
+
* Uses inset shadow for the element's own background (to avoid using background-color)
|
|
999
|
+
* and regular shadows for vertical extensions
|
|
1000
|
+
*/
|
|
1001
|
+
private buildBoxShadow(topExtension: number, bottomExtension: number, bgColor: string): string {
|
|
1002
|
+
const shadows: string[] = [];
|
|
1003
|
+
|
|
1004
|
+
// Use inset shadow to create the background color effect
|
|
1005
|
+
// This replaces background-color to avoid overlap issues between spans
|
|
1006
|
+
shadows.push(`inset 0 0 0 9999px ${bgColor}`);
|
|
1007
|
+
|
|
1008
|
+
// Add vertical extensions
|
|
1009
|
+
if (bottomExtension > 0) {
|
|
1010
|
+
shadows.push(`0 ${bottomExtension}px 0 ${bgColor}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (topExtension > 0) {
|
|
1013
|
+
shadows.push(`0 -${topExtension}px 0 ${bgColor}`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return shadows.join(', ');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Removes fake background wrapper (legacy support)
|
|
1021
|
+
* @param element - wrapper element
|
|
1022
|
+
*/
|
|
1023
|
+
private unwrapFakeBackground(element: HTMLElement): void {
|
|
1024
|
+
const parent = element.parentNode;
|
|
1025
|
+
|
|
1026
|
+
if (!parent) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
while (element.firstChild) {
|
|
1031
|
+
parent.insertBefore(element.firstChild, element);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
parent.removeChild(element);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Save SelectionUtils's range
|
|
1039
|
+
*/
|
|
1040
|
+
public save(): void {
|
|
1041
|
+
this.savedSelectionRange = SelectionUtils.range;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Restore saved SelectionUtils's range
|
|
1046
|
+
*/
|
|
1047
|
+
public restore(): void {
|
|
1048
|
+
if (!this.savedSelectionRange) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const sel = window.getSelection();
|
|
1053
|
+
|
|
1054
|
+
if (!sel) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
sel.removeAllRanges();
|
|
1059
|
+
sel.addRange(this.savedSelectionRange);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Clears saved selection
|
|
1064
|
+
*/
|
|
1065
|
+
public clearSaved(): void {
|
|
1066
|
+
this.savedSelectionRange = null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Collapse current selection
|
|
1071
|
+
*/
|
|
1072
|
+
public collapseToEnd(): void {
|
|
1073
|
+
const sel = window.getSelection();
|
|
1074
|
+
|
|
1075
|
+
if (!sel || !sel.focusNode) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const range = document.createRange();
|
|
1080
|
+
|
|
1081
|
+
range.selectNodeContents(sel.focusNode);
|
|
1082
|
+
range.collapse(false);
|
|
1083
|
+
sel.removeAllRanges();
|
|
1084
|
+
sel.addRange(range);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Looks ahead to find passed tag from current selection
|
|
1089
|
+
* @param {string} tagName - tag to found
|
|
1090
|
+
* @param {string} [className] - tag's class name
|
|
1091
|
+
* @param {number} [searchDepth] - count of tags that can be included. For better performance.
|
|
1092
|
+
* @returns {HTMLElement|null}
|
|
1093
|
+
*/
|
|
1094
|
+
public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
|
|
1095
|
+
const selection = window.getSelection();
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* If selection is missing or no anchorNode or focusNode were found then return null
|
|
1099
|
+
*/
|
|
1100
|
+
if (!selection || !selection.anchorNode || !selection.focusNode) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Define Nodes for start and end of selection
|
|
1106
|
+
*/
|
|
1107
|
+
const boundNodes = [
|
|
1108
|
+
/** the Node in which the selection begins */
|
|
1109
|
+
selection.anchorNode as HTMLElement,
|
|
1110
|
+
/** the Node in which the selection ends */
|
|
1111
|
+
selection.focusNode as HTMLElement,
|
|
1112
|
+
];
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Helper function to find parent tag starting from a given node
|
|
1116
|
+
* @param {HTMLElement} startNode - node to start searching from
|
|
1117
|
+
* @returns {HTMLElement | null}
|
|
1118
|
+
*/
|
|
1119
|
+
const findTagFromNode = (startNode: HTMLElement): HTMLElement | null => {
|
|
1120
|
+
const searchUpTree = (node: HTMLElement, depth: number): HTMLElement | null => {
|
|
1121
|
+
if (depth <= 0 || !node) {
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Check if the current node itself matches the tag (for element nodes).
|
|
1127
|
+
* This handles the case when the selection anchor/focus is the target element.
|
|
1128
|
+
*/
|
|
1129
|
+
const isCurrentNodeMatch = node.nodeType === Node.ELEMENT_NODE && node.tagName === tagName;
|
|
1130
|
+
const currentNodeHasMatchingClass = !className || (node.classList && node.classList.contains(className));
|
|
1131
|
+
|
|
1132
|
+
if (isCurrentNodeMatch && currentNodeHasMatchingClass) {
|
|
1133
|
+
return node;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!node.parentNode) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const parent = node.parentNode as HTMLElement;
|
|
1141
|
+
|
|
1142
|
+
const hasMatchingClass = !className || (parent.classList && parent.classList.contains(className));
|
|
1143
|
+
const hasMatchingTag = parent.tagName === tagName;
|
|
1144
|
+
|
|
1145
|
+
if (hasMatchingTag && hasMatchingClass) {
|
|
1146
|
+
return parent;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return searchUpTree(parent, depth - 1);
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
return searchUpTree(startNode, searchDepth);
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* For each selection parent Nodes we try to find target tag [with target class name]
|
|
1157
|
+
*/
|
|
1158
|
+
for (const node of boundNodes) {
|
|
1159
|
+
const foundTag = findTagFromNode(node);
|
|
1160
|
+
|
|
1161
|
+
if (foundTag) {
|
|
1162
|
+
return foundTag;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Return null if tag was not found
|
|
1168
|
+
*/
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Expands selection range to the passed parent node
|
|
1174
|
+
* @param {HTMLElement} element - element which contents should be selected
|
|
1175
|
+
*/
|
|
1176
|
+
public expandToTag(element: HTMLElement): void {
|
|
1177
|
+
const selection = window.getSelection();
|
|
1178
|
+
|
|
1179
|
+
if (!selection) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
selection.removeAllRanges();
|
|
1184
|
+
const range = document.createRange();
|
|
1185
|
+
|
|
1186
|
+
range.selectNodeContents(element);
|
|
1187
|
+
selection.addRange(range);
|
|
1188
|
+
}
|
|
1189
|
+
}
|