@jackuait/blok 0.4.1-beta.1 → 0.4.1-beta.12
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 +138 -17
- package/codemod/README.md +45 -7
- package/codemod/migrate-editorjs-to-blok.js +960 -92
- package/codemod/test.js +780 -77
- package/dist/blok.mjs +5 -2
- package/dist/chunks/blok-BU6NwVkN.mjs +13239 -0
- package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
- package/dist/chunks/i18next-loader-D8GzSwio.mjs +43 -0
- package/dist/{index-CEXLTV6f.mjs → chunks/index-C5e_WLFg.mjs} +2 -2
- package/dist/chunks/inline-tool-convert-CLUxkCe_.mjs +1990 -0
- package/dist/chunks/messages-0tDXLuyH.mjs +48 -0
- package/dist/chunks/messages-2_xedlYw.mjs +48 -0
- package/dist/chunks/messages-AHESHJm_.mjs +48 -0
- package/dist/chunks/messages-B5hdXZwA.mjs +48 -0
- package/dist/chunks/messages-B5jGUnOy.mjs +48 -0
- package/dist/chunks/messages-B5puUm7R.mjs +48 -0
- package/dist/chunks/messages-B66ZSDCJ.mjs +48 -0
- package/dist/chunks/messages-B9Oba7sq.mjs +48 -0
- package/dist/chunks/messages-BA0rcTCY.mjs +48 -0
- package/dist/chunks/messages-BBJgd5jG.mjs +48 -0
- package/dist/chunks/messages-BPqWKx5Z.mjs +48 -0
- package/dist/chunks/messages-Bdv-IkfG.mjs +48 -0
- package/dist/chunks/messages-BeUhMpsr.mjs +48 -0
- package/dist/chunks/messages-Bf6Y3_GI.mjs +48 -0
- package/dist/chunks/messages-BiExzWJv.mjs +48 -0
- package/dist/chunks/messages-BlpqL8vG.mjs +48 -0
- package/dist/chunks/messages-BmKCChWZ.mjs +48 -0
- package/dist/chunks/messages-Bn253WWC.mjs +48 -0
- package/dist/chunks/messages-BrJHUxQL.mjs +48 -0
- package/dist/chunks/messages-C5b7hr_E.mjs +48 -0
- package/dist/chunks/messages-C7I_AVH2.mjs +48 -0
- package/dist/chunks/messages-CJoBtXU6.mjs +48 -0
- package/dist/chunks/messages-CQj2JU2j.mjs +48 -0
- package/dist/chunks/messages-CUZ1x1QD.mjs +48 -0
- package/dist/chunks/messages-CUy1vn-b.mjs +48 -0
- package/dist/chunks/messages-CVeWVKsV.mjs +48 -0
- package/dist/chunks/messages-CXHd9SUK.mjs +48 -0
- package/dist/chunks/messages-CbMyJSzS.mjs +48 -0
- package/dist/chunks/messages-CbhuIWRJ.mjs +48 -0
- package/dist/chunks/messages-CeCjVKMW.mjs +48 -0
- package/dist/chunks/messages-Cj-t1bdy.mjs +48 -0
- package/dist/chunks/messages-CkFT2gle.mjs +48 -0
- package/dist/chunks/messages-Cm9aLHeX.mjs +48 -0
- package/dist/chunks/messages-CnvW8Slp.mjs +48 -0
- package/dist/chunks/messages-Cr-RJ7YB.mjs +48 -0
- package/dist/chunks/messages-CrsJ1TEJ.mjs +48 -0
- package/dist/chunks/messages-Cu08aLS3.mjs +48 -0
- package/dist/chunks/messages-CvaqJFN-.mjs +48 -0
- package/dist/chunks/messages-CyDU5lz9.mjs +48 -0
- package/dist/chunks/messages-CySyfkMU.mjs +48 -0
- package/dist/chunks/messages-Cyi2AMmz.mjs +48 -0
- package/dist/chunks/messages-D00OjS2n.mjs +48 -0
- package/dist/chunks/messages-DDLgIPDF.mjs +48 -0
- package/dist/chunks/messages-DMQIHGRj.mjs +48 -0
- package/dist/chunks/messages-DOlC_Tty.mjs +48 -0
- package/dist/chunks/messages-DV6shA9b.mjs +48 -0
- package/dist/chunks/messages-DY94ykcE.mjs +48 -0
- package/dist/chunks/messages-DbVquYKN.mjs +48 -0
- package/dist/chunks/messages-DcKOuncK.mjs +48 -0
- package/dist/chunks/messages-Dg92dXZ5.mjs +48 -0
- package/dist/chunks/messages-DnbbyJT3.mjs +48 -0
- package/dist/chunks/messages-DteYq0rv.mjs +48 -0
- package/dist/chunks/messages-GC2PhgV3.mjs +48 -0
- package/dist/chunks/messages-JGsXAReJ.mjs +48 -0
- package/dist/chunks/messages-JZUhXTuV.mjs +48 -0
- package/dist/chunks/messages-LvFKBBPa.mjs +48 -0
- package/dist/chunks/messages-NP1myMGI.mjs +48 -0
- package/dist/chunks/messages-Q4kc_ZtL.mjs +48 -0
- package/dist/chunks/messages-RvMHb2Ht.mjs +48 -0
- package/dist/chunks/messages-ftMcCEuO.mjs +48 -0
- package/dist/chunks/messages-o24dK6CU.mjs +48 -0
- package/dist/chunks/messages-pA5TvcAj.mjs +48 -0
- package/dist/chunks/messages-rRSHQDCX.mjs +48 -0
- package/dist/chunks/messages-srxrv8Yh.mjs +48 -0
- package/dist/chunks/messages-wdqp4610.mjs +48 -0
- package/dist/chunks/messages-zS1AXZ0y.mjs +48 -0
- package/dist/chunks/messages-zSzDzXej.mjs +48 -0
- package/dist/full.mjs +50 -0
- package/dist/locales.mjs +228 -0
- package/dist/messages-0tDXLuyH.mjs +48 -0
- package/dist/messages-2_xedlYw.mjs +48 -0
- package/dist/messages-AHESHJm_.mjs +48 -0
- package/dist/messages-B5hdXZwA.mjs +48 -0
- package/dist/messages-B5jGUnOy.mjs +48 -0
- package/dist/messages-B5puUm7R.mjs +48 -0
- package/dist/messages-B66ZSDCJ.mjs +48 -0
- package/dist/messages-B9Oba7sq.mjs +48 -0
- package/dist/messages-BA0rcTCY.mjs +48 -0
- package/dist/messages-BBJgd5jG.mjs +48 -0
- package/dist/messages-BPqWKx5Z.mjs +48 -0
- package/dist/messages-Bdv-IkfG.mjs +48 -0
- package/dist/messages-BeUhMpsr.mjs +48 -0
- package/dist/messages-Bf6Y3_GI.mjs +48 -0
- package/dist/messages-BiExzWJv.mjs +48 -0
- package/dist/messages-BlpqL8vG.mjs +48 -0
- package/dist/messages-BmKCChWZ.mjs +48 -0
- package/dist/messages-Bn253WWC.mjs +48 -0
- package/dist/messages-BrJHUxQL.mjs +48 -0
- package/dist/messages-C5b7hr_E.mjs +48 -0
- package/dist/messages-C7I_AVH2.mjs +48 -0
- package/dist/messages-CJoBtXU6.mjs +48 -0
- package/dist/messages-CQj2JU2j.mjs +48 -0
- package/dist/messages-CUZ1x1QD.mjs +48 -0
- package/dist/messages-CUy1vn-b.mjs +48 -0
- package/dist/messages-CVeWVKsV.mjs +48 -0
- package/dist/messages-CXHd9SUK.mjs +48 -0
- package/dist/messages-CbMyJSzS.mjs +48 -0
- package/dist/messages-CbhuIWRJ.mjs +48 -0
- package/dist/messages-CeCjVKMW.mjs +48 -0
- package/dist/messages-Cj-t1bdy.mjs +48 -0
- package/dist/messages-CkFT2gle.mjs +48 -0
- package/dist/messages-Cm9aLHeX.mjs +48 -0
- package/dist/messages-CnvW8Slp.mjs +48 -0
- package/dist/messages-Cr-RJ7YB.mjs +48 -0
- package/dist/messages-CrsJ1TEJ.mjs +48 -0
- package/dist/messages-Cu08aLS3.mjs +48 -0
- package/dist/messages-CvaqJFN-.mjs +48 -0
- package/dist/messages-CyDU5lz9.mjs +48 -0
- package/dist/messages-CySyfkMU.mjs +48 -0
- package/dist/messages-Cyi2AMmz.mjs +48 -0
- package/dist/messages-D00OjS2n.mjs +48 -0
- package/dist/messages-DDLgIPDF.mjs +48 -0
- package/dist/messages-DMQIHGRj.mjs +48 -0
- package/dist/messages-DOlC_Tty.mjs +48 -0
- package/dist/messages-DV6shA9b.mjs +48 -0
- package/dist/messages-DY94ykcE.mjs +48 -0
- package/dist/messages-DbVquYKN.mjs +48 -0
- package/dist/messages-DcKOuncK.mjs +48 -0
- package/dist/messages-Dg92dXZ5.mjs +48 -0
- package/dist/messages-DnbbyJT3.mjs +48 -0
- package/dist/messages-DteYq0rv.mjs +48 -0
- package/dist/messages-GC2PhgV3.mjs +48 -0
- package/dist/messages-JGsXAReJ.mjs +48 -0
- package/dist/messages-JZUhXTuV.mjs +48 -0
- package/dist/messages-LvFKBBPa.mjs +48 -0
- package/dist/messages-NP1myMGI.mjs +48 -0
- package/dist/messages-Q4kc_ZtL.mjs +48 -0
- package/dist/messages-RvMHb2Ht.mjs +48 -0
- package/dist/messages-ftMcCEuO.mjs +48 -0
- package/dist/messages-o24dK6CU.mjs +48 -0
- package/dist/messages-pA5TvcAj.mjs +48 -0
- package/dist/messages-rRSHQDCX.mjs +48 -0
- package/dist/messages-srxrv8Yh.mjs +48 -0
- package/dist/messages-wdqp4610.mjs +48 -0
- package/dist/messages-zS1AXZ0y.mjs +48 -0
- package/dist/messages-zSzDzXej.mjs +48 -0
- package/dist/tools.mjs +3126 -0
- package/dist/vendor.LICENSE.txt +26 -225
- package/package.json +63 -24
- 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 +1428 -0
- package/src/components/block-tunes/block-tune-delete.ts +51 -0
- package/src/components/blocks.ts +352 -0
- package/src/components/constants/data-attributes.ts +344 -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 +497 -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 +45 -0
- package/src/components/i18n/locales/ar/messages.json +45 -0
- package/src/components/i18n/locales/az/messages.json +45 -0
- package/src/components/i18n/locales/bg/messages.json +45 -0
- package/src/components/i18n/locales/bn/messages.json +45 -0
- package/src/components/i18n/locales/bs/messages.json +45 -0
- package/src/components/i18n/locales/cs/messages.json +45 -0
- package/src/components/i18n/locales/da/messages.json +45 -0
- package/src/components/i18n/locales/de/messages.json +45 -0
- package/src/components/i18n/locales/dv/messages.json +45 -0
- package/src/components/i18n/locales/el/messages.json +45 -0
- package/src/components/i18n/locales/en/messages.json +45 -0
- package/src/components/i18n/locales/es/messages.json +45 -0
- package/src/components/i18n/locales/et/messages.json +45 -0
- package/src/components/i18n/locales/fa/messages.json +45 -0
- package/src/components/i18n/locales/fi/messages.json +45 -0
- package/src/components/i18n/locales/fil/messages.json +45 -0
- package/src/components/i18n/locales/fr/messages.json +45 -0
- package/src/components/i18n/locales/gu/messages.json +45 -0
- package/src/components/i18n/locales/he/messages.json +45 -0
- package/src/components/i18n/locales/hi/messages.json +45 -0
- package/src/components/i18n/locales/hr/messages.json +45 -0
- package/src/components/i18n/locales/hu/messages.json +45 -0
- package/src/components/i18n/locales/hy/messages.json +45 -0
- package/src/components/i18n/locales/id/messages.json +45 -0
- package/src/components/i18n/locales/index.ts +231 -0
- package/src/components/i18n/locales/it/messages.json +45 -0
- package/src/components/i18n/locales/ja/messages.json +45 -0
- package/src/components/i18n/locales/ka/messages.json +45 -0
- package/src/components/i18n/locales/km/messages.json +45 -0
- package/src/components/i18n/locales/kn/messages.json +45 -0
- package/src/components/i18n/locales/ko/messages.json +45 -0
- package/src/components/i18n/locales/ku/messages.json +45 -0
- package/src/components/i18n/locales/lo/messages.json +45 -0
- package/src/components/i18n/locales/lt/messages.json +45 -0
- package/src/components/i18n/locales/lv/messages.json +45 -0
- package/src/components/i18n/locales/mk/messages.json +45 -0
- package/src/components/i18n/locales/ml/messages.json +45 -0
- package/src/components/i18n/locales/mn/messages.json +45 -0
- package/src/components/i18n/locales/mr/messages.json +45 -0
- package/src/components/i18n/locales/ms/messages.json +45 -0
- package/src/components/i18n/locales/my/messages.json +45 -0
- package/src/components/i18n/locales/ne/messages.json +45 -0
- package/src/components/i18n/locales/nl/messages.json +45 -0
- package/src/components/i18n/locales/no/messages.json +45 -0
- package/src/components/i18n/locales/pa/messages.json +45 -0
- package/src/components/i18n/locales/pl/messages.json +45 -0
- package/src/components/i18n/locales/ps/messages.json +45 -0
- package/src/components/i18n/locales/pt/messages.json +45 -0
- package/src/components/i18n/locales/ro/messages.json +45 -0
- package/src/components/i18n/locales/ru/messages.json +45 -0
- package/src/components/i18n/locales/sd/messages.json +45 -0
- package/src/components/i18n/locales/si/messages.json +45 -0
- package/src/components/i18n/locales/sk/messages.json +45 -0
- package/src/components/i18n/locales/sl/messages.json +45 -0
- package/src/components/i18n/locales/sq/messages.json +45 -0
- package/src/components/i18n/locales/sr/messages.json +45 -0
- package/src/components/i18n/locales/sv/messages.json +45 -0
- package/src/components/i18n/locales/sw/messages.json +45 -0
- package/src/components/i18n/locales/ta/messages.json +45 -0
- package/src/components/i18n/locales/te/messages.json +45 -0
- package/src/components/i18n/locales/th/messages.json +45 -0
- package/src/components/i18n/locales/tr/messages.json +45 -0
- package/src/components/i18n/locales/ug/messages.json +45 -0
- package/src/components/i18n/locales/uk/messages.json +45 -0
- package/src/components/i18n/locales/ur/messages.json +45 -0
- package/src/components/i18n/locales/vi/messages.json +45 -0
- package/src/components/i18n/locales/yi/messages.json +45 -0
- package/src/components/i18n/locales/zh/messages.json +45 -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 +142 -0
- package/src/components/inline-tools/inline-tool-italic.ts +500 -0
- package/src/components/inline-tools/inline-tool-link.ts +540 -0
- package/src/components/modules/api/blocks.ts +377 -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 +35 -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 +1591 -0
- package/src/components/modules/blockManager.ts +1356 -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 +1204 -0
- package/src/components/modules/history.ts +1098 -0
- package/src/components/modules/i18n.ts +332 -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 +711 -0
- package/src/components/modules/renderer.ts +155 -0
- package/src/components/modules/saver.ts +283 -0
- package/src/components/modules/toolbar/blockSettings.ts +782 -0
- package/src/components/modules/toolbar/index.ts +1296 -0
- package/src/components/modules/toolbar/inline.ts +956 -0
- package/src/components/modules/tools.ts +625 -0
- package/src/components/modules/ui.ts +1283 -0
- package/src/components/polyfills.ts +113 -0
- package/src/components/selection.ts +1179 -0
- package/src/components/tools/base.ts +301 -0
- package/src/components/tools/block.ts +339 -0
- package/src/components/tools/collection.ts +67 -0
- package/src/components/tools/factory.ts +138 -0
- package/src/components/tools/inline.ts +71 -0
- package/src/components/tools/tune.ts +33 -0
- package/src/components/ui/toolbox.ts +610 -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 +680 -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 +197 -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 +178 -0
- package/src/components/utils/popover/components/search-input/search-input.types.ts +59 -0
- package/src/components/utils/popover/index.ts +13 -0
- package/src/components/utils/popover/popover-abstract.ts +457 -0
- package/src/components/utils/popover/popover-desktop.ts +682 -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 +110 -0
- package/src/components/utils/tooltip.ts +591 -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 +759 -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 +126 -0
- package/src/tools/header/index.ts +647 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/list/index.ts +1826 -0
- package/src/tools/paragraph/index.ts +412 -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 +9 -1
- package/types/api/history.d.ts +7 -0
- package/types/api/i18n.d.ts +22 -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/configs/sanitizer-config.d.ts +25 -1
- package/types/data-attributes.d.ts +170 -0
- package/types/data-formats/output-data.d.ts +15 -0
- package/types/full.d.ts +80 -0
- package/types/index.d.ts +30 -13
- 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 +11 -2
- 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 +99 -6
- package/types/tools/tool.d.ts +6 -0
- package/types/tools-entry.d.ts +49 -0
- package/types/utils/popover/popover-item.d.ts +24 -5
- package/types/utils/popover/popover.d.ts +13 -0
- package/dist/blok-C8XbyLHh.mjs +0 -25795
- package/dist/blok.umd.js +0 -181
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Module UI
|
|
4
|
+
* @type {UI}
|
|
5
|
+
*/
|
|
6
|
+
import { Module } from '../__module';
|
|
7
|
+
import { Dom as $, toggleEmptyMark } from '../dom';
|
|
8
|
+
import { debounce, getValidUrl, isEmpty, openTab, throttle } from '../utils';
|
|
9
|
+
|
|
10
|
+
import { SelectionUtils as Selection } from '../selection';
|
|
11
|
+
import { Flipper } from '../flipper';
|
|
12
|
+
import type { Block } from '../block';
|
|
13
|
+
import { mobileScreenBreakpoint } from '../utils';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Horizontal distance from the content edge where block hover is still detected.
|
|
17
|
+
* Extends to the left for LTR layouts, to the right for RTL.
|
|
18
|
+
*/
|
|
19
|
+
const HOVER_ZONE_SIZE = 100;
|
|
20
|
+
|
|
21
|
+
import styles from '../../styles/main.css?inline';
|
|
22
|
+
import { BlockHovered } from '../events/BlockHovered';
|
|
23
|
+
import {
|
|
24
|
+
DATA_ATTR,
|
|
25
|
+
BLOK_INTERFACE_VALUE,
|
|
26
|
+
selectionChangeDebounceTimeout,
|
|
27
|
+
} from '../constants';
|
|
28
|
+
import { BlokMobileLayoutToggled } from '../events';
|
|
29
|
+
import { destroyAnnouncer, registerAnnouncer } from '../utils/announcer';
|
|
30
|
+
/**
|
|
31
|
+
* HTML Elements used for UI
|
|
32
|
+
*/
|
|
33
|
+
interface UINodes {
|
|
34
|
+
holder: HTMLElement;
|
|
35
|
+
wrapper: HTMLElement;
|
|
36
|
+
redactor: HTMLElement;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @class
|
|
41
|
+
* @classdesc Makes Blok UI:
|
|
42
|
+
* <blok-editor>
|
|
43
|
+
* <blok-redactor />
|
|
44
|
+
* <blok-toolbar />
|
|
45
|
+
* <blok-inline-toolbar />
|
|
46
|
+
* </blok-editor>
|
|
47
|
+
* @typedef {UI} UI
|
|
48
|
+
* @property {BlokConfig} config - blok configuration {@link Blok#configuration}
|
|
49
|
+
* @property {object} Blok - available blok modules {@link Blok#moduleInstances}
|
|
50
|
+
* @property {object} nodes -
|
|
51
|
+
* @property {Element} nodes.holder - element where we need to append redactor
|
|
52
|
+
* @property {Element} nodes.wrapper - <blok-editor>
|
|
53
|
+
* @property {Element} nodes.redactor - <blok-redactor>
|
|
54
|
+
*/
|
|
55
|
+
export class UI extends Module<UINodes> {
|
|
56
|
+
/**
|
|
57
|
+
* Return Width of center column of Blok
|
|
58
|
+
* @returns {DOMRect}
|
|
59
|
+
*/
|
|
60
|
+
public get contentRect(): DOMRect {
|
|
61
|
+
if (this.contentRectCache !== null) {
|
|
62
|
+
return this.contentRectCache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const someBlock = this.nodes.wrapper.querySelector('[data-blok-testid="block-content"]');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* When Blok is not ready, there is no Blocks, so return the default value
|
|
69
|
+
*/
|
|
70
|
+
if (!someBlock) {
|
|
71
|
+
return {
|
|
72
|
+
width: 650,
|
|
73
|
+
left: 0,
|
|
74
|
+
right: 0,
|
|
75
|
+
} as DOMRect;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.contentRectCache = someBlock.getBoundingClientRect();
|
|
79
|
+
|
|
80
|
+
return this.contentRectCache;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Flag that became true on mobile viewport
|
|
85
|
+
* @type {boolean}
|
|
86
|
+
*/
|
|
87
|
+
public isMobile = false;
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cache for center column rectangle info
|
|
92
|
+
* Invalidates on window resize
|
|
93
|
+
* @type {DOMRect}
|
|
94
|
+
*/
|
|
95
|
+
private contentRectCache: DOMRect | null = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handle window resize only when it finished
|
|
99
|
+
* @type {() => void}
|
|
100
|
+
*/
|
|
101
|
+
private resizeDebouncer: () => void = debounce(() => {
|
|
102
|
+
this.windowResize();
|
|
103
|
+
|
|
104
|
+
}, 200);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle selection change to manipulate Inline Toolbar appearance
|
|
108
|
+
*/
|
|
109
|
+
private selectionChangeDebounced = debounce(() => {
|
|
110
|
+
this.selectionChanged();
|
|
111
|
+
}, selectionChangeDebounceTimeout);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Making main interface
|
|
115
|
+
*/
|
|
116
|
+
public async prepare(): Promise<void> {
|
|
117
|
+
/**
|
|
118
|
+
* Detect mobile version
|
|
119
|
+
*/
|
|
120
|
+
this.setIsMobile();
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Make main UI elements
|
|
124
|
+
*/
|
|
125
|
+
this.make();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load and append CSS
|
|
129
|
+
*/
|
|
130
|
+
this.loadStyles();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register this Blok instance with the accessibility announcer
|
|
134
|
+
* for proper multi-instance cleanup
|
|
135
|
+
*/
|
|
136
|
+
registerAnnouncer();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Toggle read-only state
|
|
141
|
+
*
|
|
142
|
+
* If readOnly is true:
|
|
143
|
+
* - removes all listeners from main UI module elements
|
|
144
|
+
*
|
|
145
|
+
* if readOnly is false:
|
|
146
|
+
* - enables all listeners to UI module elements
|
|
147
|
+
* @param {boolean} readOnlyEnabled - "read only" state
|
|
148
|
+
*/
|
|
149
|
+
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
|
150
|
+
/**
|
|
151
|
+
* Prepare components based on read-only state
|
|
152
|
+
*/
|
|
153
|
+
if (readOnlyEnabled) {
|
|
154
|
+
/**
|
|
155
|
+
* Unbind all events
|
|
156
|
+
*
|
|
157
|
+
*/
|
|
158
|
+
this.unbindReadOnlySensitiveListeners();
|
|
159
|
+
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bindListeners = (): void => {
|
|
164
|
+
/**
|
|
165
|
+
* Bind events for the UI elements
|
|
166
|
+
*/
|
|
167
|
+
this.bindReadOnlySensitiveListeners();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Ensure listeners are attached immediately for interactive use.
|
|
172
|
+
*/
|
|
173
|
+
bindListeners();
|
|
174
|
+
|
|
175
|
+
const idleCallback = window.requestIdleCallback;
|
|
176
|
+
|
|
177
|
+
if (typeof idleCallback !== 'function') {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Re-bind on idle to preserve historical behavior when additional nodes appear later.
|
|
183
|
+
*/
|
|
184
|
+
idleCallback(bindListeners, {
|
|
185
|
+
timeout: 2000,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if Blok is empty and set data attribute on wrapper
|
|
191
|
+
*/
|
|
192
|
+
public checkEmptiness(): void {
|
|
193
|
+
const { BlockManager } = this.Blok;
|
|
194
|
+
|
|
195
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.empty, BlockManager.isBlokEmpty ? 'true' : 'false');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if one of Toolbar is opened
|
|
200
|
+
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
public get someToolbarOpened(): boolean {
|
|
204
|
+
const { Toolbar, BlockSettings, InlineToolbar } = this.Blok;
|
|
205
|
+
|
|
206
|
+
return Boolean(BlockSettings.opened || InlineToolbar.opened || Toolbar.toolbox.opened);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check for some Flipper-buttons is under focus
|
|
211
|
+
*/
|
|
212
|
+
public get someFlipperButtonFocused(): boolean {
|
|
213
|
+
/**
|
|
214
|
+
* Toolbar has internal module (Toolbox) that has own Flipper,
|
|
215
|
+
* so we check it manually
|
|
216
|
+
*/
|
|
217
|
+
if (this.Blok.Toolbar.toolbox.hasFocus()) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
return Object.entries(this.Blok).filter(([_moduleName, moduleClass]) => {
|
|
223
|
+
return moduleClass.flipper instanceof Flipper;
|
|
224
|
+
})
|
|
225
|
+
.some(([_moduleName, moduleClass]) => {
|
|
226
|
+
return moduleClass.flipper.hasFocus();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Clean blok`s UI
|
|
234
|
+
*/
|
|
235
|
+
public destroy(): void {
|
|
236
|
+
this.nodes.holder.innerHTML = '';
|
|
237
|
+
|
|
238
|
+
this.unbindReadOnlyInsensitiveListeners();
|
|
239
|
+
|
|
240
|
+
// Clean up accessibility announcer
|
|
241
|
+
destroyAnnouncer();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Close all Blok's toolbars
|
|
246
|
+
*/
|
|
247
|
+
public closeAllToolbars(): void {
|
|
248
|
+
const { Toolbar, BlockSettings, InlineToolbar } = this.Blok;
|
|
249
|
+
|
|
250
|
+
BlockSettings.close();
|
|
251
|
+
InlineToolbar.close();
|
|
252
|
+
Toolbar.toolbox.close();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Event listener for 'mousedown' and 'touchstart' events
|
|
257
|
+
* @param event - TouchEvent or MouseEvent
|
|
258
|
+
*/
|
|
259
|
+
private documentTouchedListener = (event: Event): void => {
|
|
260
|
+
this.documentTouched(event);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check for mobile mode and save the result
|
|
265
|
+
*/
|
|
266
|
+
private setIsMobile(): void {
|
|
267
|
+
const isMobile = window.innerWidth < mobileScreenBreakpoint;
|
|
268
|
+
|
|
269
|
+
if (isMobile !== this.isMobile) {
|
|
270
|
+
/**
|
|
271
|
+
* Dispatch global event
|
|
272
|
+
*/
|
|
273
|
+
this.eventsDispatcher.emit(BlokMobileLayoutToggled, {
|
|
274
|
+
isEnabled: this.isMobile,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.isMobile = isMobile;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Makes Blok interface
|
|
283
|
+
*/
|
|
284
|
+
private make(): void {
|
|
285
|
+
/**
|
|
286
|
+
* Element where we need to append Blok
|
|
287
|
+
* @type {Element}
|
|
288
|
+
*/
|
|
289
|
+
const holder = this.config.holder;
|
|
290
|
+
|
|
291
|
+
if (!holder) {
|
|
292
|
+
throw new Error('Blok holder is not specified in the configuration.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.nodes.holder = $.getHolder(holder);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create and save main UI elements
|
|
299
|
+
*/
|
|
300
|
+
this.nodes.wrapper = $.make('div', [
|
|
301
|
+
'group',
|
|
302
|
+
'relative',
|
|
303
|
+
'box-border',
|
|
304
|
+
'z-[1]',
|
|
305
|
+
'[&[data-blok-dragging=true]]:cursor-grabbing',
|
|
306
|
+
// SVG defaults
|
|
307
|
+
'[&_svg]:max-h-full',
|
|
308
|
+
'[&_path]:stroke-current',
|
|
309
|
+
// Native selection color
|
|
310
|
+
'[&_::selection]:bg-selection-inline',
|
|
311
|
+
// Hide placeholder when toolbox is opened
|
|
312
|
+
'[&[data-blok-toolbox-opened=true]_[contentEditable=true][data-blok-placeholder]:focus]:before:!opacity-0',
|
|
313
|
+
...(this.isRtl ? [ '[direction:rtl]' ] : []),
|
|
314
|
+
]);
|
|
315
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.interface, BLOK_INTERFACE_VALUE);
|
|
316
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.editor, '');
|
|
317
|
+
this.nodes.wrapper.setAttribute('data-blok-testid', 'blok-editor');
|
|
318
|
+
if (this.isRtl) {
|
|
319
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.rtl, 'true');
|
|
320
|
+
}
|
|
321
|
+
this.nodes.redactor = $.make('div', [
|
|
322
|
+
// Narrow mode: add right margin on non-mobile screens
|
|
323
|
+
'not-mobile:group-data-[blok-narrow=true]:mr-[theme(spacing.narrow-mode-right-padding)]',
|
|
324
|
+
// RTL narrow mode: add left margin instead
|
|
325
|
+
'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:ml-[theme(spacing.narrow-mode-right-padding)]',
|
|
326
|
+
'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:mr-0',
|
|
327
|
+
// Firefox empty contenteditable fix
|
|
328
|
+
'[&_[contenteditable]:empty]:after:content-["\\feff_"]',
|
|
329
|
+
]);
|
|
330
|
+
this.nodes.redactor.setAttribute(DATA_ATTR.redactor, '');
|
|
331
|
+
this.nodes.redactor.setAttribute('data-blok-testid', 'redactor');
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* If Blok has injected into the narrow container, enable Narrow Mode
|
|
335
|
+
* @todo Forced layout. Get rid of this feature
|
|
336
|
+
*/
|
|
337
|
+
if (this.nodes.holder.offsetWidth < this.contentRect.width) {
|
|
338
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.narrow, 'true');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Set customizable bottom zone height
|
|
343
|
+
*/
|
|
344
|
+
this.nodes.redactor.style.paddingBottom = this.config.minHeight + 'px';
|
|
345
|
+
|
|
346
|
+
this.nodes.wrapper.appendChild(this.nodes.redactor);
|
|
347
|
+
this.nodes.holder.appendChild(this.nodes.wrapper);
|
|
348
|
+
|
|
349
|
+
this.bindReadOnlyInsensitiveListeners();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Appends CSS
|
|
354
|
+
*/
|
|
355
|
+
private loadStyles(): void {
|
|
356
|
+
/**
|
|
357
|
+
* Load CSS
|
|
358
|
+
*/
|
|
359
|
+
|
|
360
|
+
const styleTagId = 'blok-styles';
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Do not append styles again if they are already on the page
|
|
364
|
+
*/
|
|
365
|
+
if ($.get(styleTagId)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Make tag
|
|
371
|
+
*/
|
|
372
|
+
const tag = $.make('style', null, {
|
|
373
|
+
id: styleTagId,
|
|
374
|
+
textContent: styles.toString(),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* If user enabled Content Security Policy, he can pass nonce through the config
|
|
379
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
|
|
380
|
+
*/
|
|
381
|
+
if (this.config.style && !isEmpty(this.config.style) && this.config.style.nonce) {
|
|
382
|
+
tag.setAttribute('nonce', this.config.style.nonce);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Append styles at the top of HEAD tag
|
|
387
|
+
*/
|
|
388
|
+
$.prepend(document.head, tag);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Adds listeners that should work both in read-only and read-write modes
|
|
393
|
+
*/
|
|
394
|
+
private bindReadOnlyInsensitiveListeners(): void {
|
|
395
|
+
this.listeners.on(document, 'selectionchange', this.selectionChangeDebounced);
|
|
396
|
+
|
|
397
|
+
this.listeners.on(window, 'resize', this.resizeDebouncer, {
|
|
398
|
+
passive: true,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.listeners.on(this.nodes.redactor, 'mousedown', this.documentTouchedListener, {
|
|
402
|
+
capture: true,
|
|
403
|
+
passive: true,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
this.listeners.on(this.nodes.redactor, 'touchstart', this.documentTouchedListener, {
|
|
407
|
+
capture: true,
|
|
408
|
+
passive: true,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Removes listeners that should work both in read-only and read-write modes
|
|
414
|
+
*/
|
|
415
|
+
private unbindReadOnlyInsensitiveListeners(): void {
|
|
416
|
+
this.listeners.off(document, 'selectionchange', this.selectionChangeDebounced);
|
|
417
|
+
this.listeners.off(window, 'resize', this.resizeDebouncer);
|
|
418
|
+
this.listeners.off(this.nodes.redactor, 'mousedown', this.documentTouchedListener);
|
|
419
|
+
this.listeners.off(this.nodes.redactor, 'touchstart', this.documentTouchedListener);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Adds listeners that should work only in read-only mode
|
|
425
|
+
*/
|
|
426
|
+
private bindReadOnlySensitiveListeners(): void {
|
|
427
|
+
this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: Event) => {
|
|
428
|
+
if (event instanceof MouseEvent) {
|
|
429
|
+
this.redactorClicked(event);
|
|
430
|
+
}
|
|
431
|
+
}, false);
|
|
432
|
+
|
|
433
|
+
this.readOnlyMutableListeners.on(document, 'keydown', (event: Event) => {
|
|
434
|
+
if (event instanceof KeyboardEvent) {
|
|
435
|
+
this.documentKeydown(event);
|
|
436
|
+
}
|
|
437
|
+
}, true);
|
|
438
|
+
|
|
439
|
+
this.readOnlyMutableListeners.on(document, 'mousedown', (event: Event) => {
|
|
440
|
+
if (event instanceof MouseEvent) {
|
|
441
|
+
this.documentClicked(event);
|
|
442
|
+
}
|
|
443
|
+
}, true);
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Start watching 'block-hovered' events that is used by Toolbar for moving
|
|
447
|
+
*/
|
|
448
|
+
this.watchBlockHoveredEvents();
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* We have custom logic for providing placeholders for contenteditable elements.
|
|
452
|
+
* To make it work, we need to have data-blok-empty mark on empty inputs.
|
|
453
|
+
*/
|
|
454
|
+
this.enableInputsEmptyMark();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Listen redactor mousemove to emit 'block-hovered' event
|
|
460
|
+
*/
|
|
461
|
+
private watchBlockHoveredEvents(): void {
|
|
462
|
+
/**
|
|
463
|
+
* Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove.
|
|
464
|
+
* Stores block ID to ensure consistent comparison regardless of how the block was detected.
|
|
465
|
+
*/
|
|
466
|
+
const blockHoveredState: { lastHoveredBlockId: string | null } = {
|
|
467
|
+
lastHoveredBlockId: null,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const handleBlockHovered = (event: Event): void => {
|
|
471
|
+
if (typeof MouseEvent === 'undefined' || !(event instanceof MouseEvent)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const hoveredBlockElement = (event.target as Element | null)?.closest('[data-blok-testid="block-wrapper"]');
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* If no block element found directly, try the extended hover zone
|
|
479
|
+
*/
|
|
480
|
+
const zoneBlock = !hoveredBlockElement
|
|
481
|
+
? this.findBlockInHoverZone(event.clientX, event.clientY)
|
|
482
|
+
: null;
|
|
483
|
+
|
|
484
|
+
if (zoneBlock !== null && blockHoveredState.lastHoveredBlockId !== zoneBlock.id) {
|
|
485
|
+
blockHoveredState.lastHoveredBlockId = zoneBlock.id;
|
|
486
|
+
|
|
487
|
+
this.eventsDispatcher.emit(BlockHovered, {
|
|
488
|
+
block: zoneBlock,
|
|
489
|
+
target: zoneBlock.holder,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (zoneBlock !== null) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!hoveredBlockElement) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const block = this.Blok.BlockManager.getBlockByChildNode(hoveredBlockElement);
|
|
502
|
+
|
|
503
|
+
if (!block) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* For multi-block selection, still emit 'block-hovered' event so toolbar can follow the hovered block.
|
|
509
|
+
* The toolbar module will handle the logic of whether to move or not.
|
|
510
|
+
*/
|
|
511
|
+
if (blockHoveredState.lastHoveredBlockId === block.id) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
blockHoveredState.lastHoveredBlockId = block.id;
|
|
516
|
+
|
|
517
|
+
this.eventsDispatcher.emit(BlockHovered, {
|
|
518
|
+
block,
|
|
519
|
+
target: event.target as Element,
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const throttledHandleBlockHovered = throttle(
|
|
524
|
+
handleBlockHovered as (...args: unknown[]) => unknown,
|
|
525
|
+
|
|
526
|
+
20
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Listen on document to detect hover in the extended zone
|
|
531
|
+
* which is outside the wrapper's bounds.
|
|
532
|
+
* We filter events to only process those over the editor or in the hover zone.
|
|
533
|
+
*/
|
|
534
|
+
this.readOnlyMutableListeners.on(document, 'mousemove', (event: Event) => {
|
|
535
|
+
throttledHandleBlockHovered(event);
|
|
536
|
+
}, {
|
|
537
|
+
passive: true,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Finds a block by vertical position when cursor is in the extended hover zone.
|
|
543
|
+
* The zone extends HOVER_ZONE_SIZE pixels from the content edge (left for LTR, right for RTL).
|
|
544
|
+
* @param clientX - Cursor X position
|
|
545
|
+
* @param clientY - Cursor Y position
|
|
546
|
+
* @returns Block at the vertical position, or null if not in hover zone or no block found
|
|
547
|
+
*/
|
|
548
|
+
private findBlockInHoverZone(clientX: number, clientY: number): Block | null {
|
|
549
|
+
const contentRect = this.contentRect;
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* For LTR: check if cursor is within hover zone to the left of content
|
|
553
|
+
* For RTL: check if cursor is within hover zone to the right of content
|
|
554
|
+
*/
|
|
555
|
+
const isInHoverZone = this.isRtl
|
|
556
|
+
? clientX > contentRect.right && clientX <= contentRect.right + HOVER_ZONE_SIZE
|
|
557
|
+
: clientX < contentRect.left && clientX >= contentRect.left - HOVER_ZONE_SIZE;
|
|
558
|
+
|
|
559
|
+
if (!isInHoverZone) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Find block by Y position
|
|
565
|
+
*/
|
|
566
|
+
for (const block of this.Blok.BlockManager.blocks) {
|
|
567
|
+
const rect = block.holder.getBoundingClientRect();
|
|
568
|
+
|
|
569
|
+
if (clientY >= rect.top && clientY <= rect.bottom) {
|
|
570
|
+
return block;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Unbind events that should work only in read-only mode
|
|
579
|
+
*/
|
|
580
|
+
private unbindReadOnlySensitiveListeners(): void {
|
|
581
|
+
this.readOnlyMutableListeners.clearAll();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Resize window handler
|
|
586
|
+
*/
|
|
587
|
+
private windowResize(): void {
|
|
588
|
+
/**
|
|
589
|
+
* Invalidate content zone size cached, because it may be changed
|
|
590
|
+
*/
|
|
591
|
+
this.contentRectCache = null;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Detect mobile version
|
|
595
|
+
*/
|
|
596
|
+
this.setIsMobile();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* All keydowns on document
|
|
601
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
602
|
+
*/
|
|
603
|
+
private documentKeydown(event: KeyboardEvent): void {
|
|
604
|
+
const key = event.key ?? '';
|
|
605
|
+
|
|
606
|
+
switch (key) {
|
|
607
|
+
case 'Enter':
|
|
608
|
+
this.enterPressed(event);
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'Backspace':
|
|
612
|
+
case 'Delete':
|
|
613
|
+
this.backspacePressed(event);
|
|
614
|
+
break;
|
|
615
|
+
|
|
616
|
+
case 'Escape':
|
|
617
|
+
this.escapePressed(event);
|
|
618
|
+
break;
|
|
619
|
+
|
|
620
|
+
case 'Tab':
|
|
621
|
+
this.tabPressed(event);
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
default:
|
|
625
|
+
this.defaultBehaviour(event);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Handle Tab key press at document level for multi-select indent/outdent
|
|
632
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
633
|
+
*/
|
|
634
|
+
private tabPressed(event: KeyboardEvent): void {
|
|
635
|
+
const { BlockSelection } = this.Blok;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Only handle Tab when blocks are selected (for multi-select indent)
|
|
639
|
+
* Otherwise, let the default behavior handle it (e.g., toolbar navigation)
|
|
640
|
+
*/
|
|
641
|
+
if (!BlockSelection.anyBlockSelected) {
|
|
642
|
+
this.defaultBehaviour(event);
|
|
643
|
+
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Forward to BlockEvents to handle the multi-select indent/outdent.
|
|
649
|
+
* BlockEvents.keydown will call preventDefault if needed.
|
|
650
|
+
*/
|
|
651
|
+
this.Blok.BlockEvents.keydown(event);
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* When blocks are selected, always prevent default Tab behavior (focus navigation)
|
|
655
|
+
* even if the indent operation couldn't be performed (e.g., mixed block types).
|
|
656
|
+
* This ensures Tab doesn't unexpectedly move focus or trigger single-block indent.
|
|
657
|
+
* We call preventDefault AFTER BlockEvents.keydown so that check for defaultPrevented passes.
|
|
658
|
+
* We also stop propagation to prevent the event from reaching block-level handlers
|
|
659
|
+
* (like ListItem's handleKeyDown) which might try to handle the Tab independently.
|
|
660
|
+
*/
|
|
661
|
+
event.preventDefault();
|
|
662
|
+
event.stopPropagation();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Ignore all other document's keydown events
|
|
667
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
668
|
+
*/
|
|
669
|
+
private defaultBehaviour(event: KeyboardEvent): void {
|
|
670
|
+
const { currentBlock } = this.Blok.BlockManager;
|
|
671
|
+
const target = event.target;
|
|
672
|
+
const isTargetElement = target instanceof HTMLElement;
|
|
673
|
+
const keyDownOnBlok = isTargetElement ? target.closest('[data-blok-testid="blok-editor"]') : null;
|
|
674
|
+
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Ignore keydowns from inside the BlockSettings popover (e.g., search input)
|
|
678
|
+
* to prevent closing the popover when typing
|
|
679
|
+
*/
|
|
680
|
+
if (isTargetElement && this.Blok.BlockSettings.contains(target)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Handle navigation mode keys even when focus is outside the editor
|
|
686
|
+
* Skip if event was already handled (e.g., by block holder listener)
|
|
687
|
+
*/
|
|
688
|
+
if (this.Blok.BlockSelection.navigationModeEnabled && !event.defaultPrevented) {
|
|
689
|
+
this.Blok.BlockEvents.keydown(event);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (this.Blok.BlockSelection.navigationModeEnabled) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* When some block is selected, but the caret is not set inside the blok, treat such keydowns as keydown on selected block.
|
|
698
|
+
*/
|
|
699
|
+
if (currentBlock !== undefined && keyDownOnBlok === null) {
|
|
700
|
+
this.Blok.BlockEvents.keydown(event);
|
|
701
|
+
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Ignore keydowns on blok and meta keys
|
|
707
|
+
*/
|
|
708
|
+
if (keyDownOnBlok || (currentBlock && isMetaKey)) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Remove all highlights and remove caret
|
|
714
|
+
*/
|
|
715
|
+
this.Blok.BlockManager.unsetCurrentBlock();
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Close Toolbar
|
|
719
|
+
*/
|
|
720
|
+
this.Blok.Toolbar.close();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
725
|
+
*/
|
|
726
|
+
private backspacePressed(event: KeyboardEvent): void {
|
|
727
|
+
/**
|
|
728
|
+
* Ignore backspace/delete from inside the BlockSettings popover (e.g., search input)
|
|
729
|
+
*/
|
|
730
|
+
if (this.Blok.BlockSettings.contains(event.target as HTMLElement)) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const { BlockManager, BlockSelection, Caret } = this.Blok;
|
|
735
|
+
|
|
736
|
+
const selectionExists = Selection.isSelectionExists;
|
|
737
|
+
const selectionCollapsed = Selection.isCollapsed;
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* If any block selected and selection doesn't exists on the page (that means no other editable element is focused),
|
|
741
|
+
* remove selected blocks
|
|
742
|
+
*/
|
|
743
|
+
const shouldRemoveSelection = BlockSelection.anyBlockSelected && (
|
|
744
|
+
!selectionExists ||
|
|
745
|
+
selectionCollapsed === true ||
|
|
746
|
+
this.Blok.CrossBlockSelection.isCrossBlockSelectionStarted
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
if (!shouldRemoveSelection) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
|
754
|
+
|
|
755
|
+
if (selectionPositionIndex === undefined) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const newBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
|
|
760
|
+
|
|
761
|
+
Caret.setToBlock(newBlock, Caret.positions.START);
|
|
762
|
+
|
|
763
|
+
/** Clear selection */
|
|
764
|
+
BlockSelection.clearSelection(event);
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Stop propagations
|
|
768
|
+
* Manipulation with BlockSelections is handled in global backspacePress because they may occur
|
|
769
|
+
* with CMD+A or RectangleSelection and they can be handled on document event
|
|
770
|
+
*/
|
|
771
|
+
event.preventDefault();
|
|
772
|
+
event.stopPropagation();
|
|
773
|
+
event.stopImmediatePropagation();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Escape pressed
|
|
778
|
+
* If some of Toolbar components are opened, then close it otherwise close Toolbar.
|
|
779
|
+
* If focus is in editor content and no toolbars are open, enable navigation mode.
|
|
780
|
+
* @param {Event} event - escape keydown event
|
|
781
|
+
*/
|
|
782
|
+
private escapePressed(event: KeyboardEvent): void {
|
|
783
|
+
/**
|
|
784
|
+
* If navigation mode is already enabled, disable it and return
|
|
785
|
+
*/
|
|
786
|
+
if (this.Blok.BlockSelection.navigationModeEnabled) {
|
|
787
|
+
this.Blok.BlockSelection.disableNavigationMode(false);
|
|
788
|
+
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Clear blocks selection by ESC (but not when entering navigation mode)
|
|
794
|
+
*/
|
|
795
|
+
if (this.Blok.BlockSelection.anyBlockSelected) {
|
|
796
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
797
|
+
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (this.Blok.Toolbar.toolbox.opened) {
|
|
802
|
+
this.Blok.Toolbar.toolbox.close();
|
|
803
|
+
this.Blok.BlockManager.currentBlock &&
|
|
804
|
+
this.Blok.Caret.setToBlock(this.Blok.BlockManager.currentBlock, this.Blok.Caret.positions.END);
|
|
805
|
+
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (this.Blok.BlockSettings.opened) {
|
|
810
|
+
this.Blok.BlockSettings.close();
|
|
811
|
+
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* If a nested popover is open (like convert-to dropdown),
|
|
817
|
+
* close only the nested popover, not the entire inline toolbar.
|
|
818
|
+
* We use stopImmediatePropagation to prevent other keydown listeners
|
|
819
|
+
* (like the one on block.holder) from also handling this event.
|
|
820
|
+
*/
|
|
821
|
+
if (this.Blok.InlineToolbar.opened && this.Blok.InlineToolbar.hasNestedPopoverOpen) {
|
|
822
|
+
event.preventDefault();
|
|
823
|
+
event.stopPropagation();
|
|
824
|
+
event.stopImmediatePropagation();
|
|
825
|
+
this.Blok.InlineToolbar.closeNestedPopover();
|
|
826
|
+
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (this.Blok.InlineToolbar.opened) {
|
|
831
|
+
this.Blok.InlineToolbar.close();
|
|
832
|
+
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* If focus is inside editor content and no toolbars are open,
|
|
838
|
+
* enable navigation mode for keyboard-based block navigation
|
|
839
|
+
*/
|
|
840
|
+
const target = event.target;
|
|
841
|
+
const isTargetElement = target instanceof HTMLElement;
|
|
842
|
+
const isInsideRedactor = isTargetElement && this.nodes.redactor.contains(target);
|
|
843
|
+
const hasCurrentBlock = this.Blok.BlockManager.currentBlock !== undefined;
|
|
844
|
+
|
|
845
|
+
if (isInsideRedactor && hasCurrentBlock) {
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
this.Blok.Toolbar.close();
|
|
848
|
+
this.Blok.BlockSelection.enableNavigationMode();
|
|
849
|
+
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
this.Blok.Toolbar.close();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Enter pressed on document
|
|
858
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
859
|
+
*/
|
|
860
|
+
private enterPressed(event: KeyboardEvent): void {
|
|
861
|
+
const { BlockManager, BlockSelection, BlockEvents } = this.Blok;
|
|
862
|
+
|
|
863
|
+
if (this.someToolbarOpened) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* If navigation mode is enabled, delegate to BlockEvents to handle Enter.
|
|
869
|
+
* This will set the caret at the end of the current block.
|
|
870
|
+
*/
|
|
871
|
+
if (BlockSelection.navigationModeEnabled) {
|
|
872
|
+
BlockEvents.keydown(event);
|
|
873
|
+
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
|
|
878
|
+
|
|
879
|
+
const selectionExists = Selection.isSelectionExists;
|
|
880
|
+
const selectionCollapsed = Selection.isCollapsed;
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* If any block selected and selection doesn't exists on the page (that means no other editable element is focused),
|
|
884
|
+
* remove selected blocks
|
|
885
|
+
*/
|
|
886
|
+
if (BlockSelection.anyBlockSelected && (!selectionExists || selectionCollapsed === true)) {
|
|
887
|
+
/** Clear selection */
|
|
888
|
+
BlockSelection.clearSelection(event);
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Stop propagations
|
|
892
|
+
* Manipulation with BlockSelections is handled in global enterPress because they may occur
|
|
893
|
+
* with CMD+A or RectangleSelection
|
|
894
|
+
*/
|
|
895
|
+
event.preventDefault();
|
|
896
|
+
event.stopImmediatePropagation();
|
|
897
|
+
event.stopPropagation();
|
|
898
|
+
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* If Caret is not set anywhere, event target on Enter is always Element that we handle
|
|
904
|
+
* In our case it is document.body
|
|
905
|
+
*
|
|
906
|
+
* So, BlockManager points some Block and Enter press is on Body
|
|
907
|
+
* We can create a new block
|
|
908
|
+
*/
|
|
909
|
+
if (!this.someToolbarOpened && hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
|
|
910
|
+
/**
|
|
911
|
+
* Insert the default typed Block
|
|
912
|
+
*/
|
|
913
|
+
const newBlock = this.Blok.BlockManager.insert();
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Prevent default enter behaviour to prevent adding a new line (<div><br></div>) to the inserted block
|
|
917
|
+
*/
|
|
918
|
+
event.preventDefault();
|
|
919
|
+
this.Blok.Caret.setToBlock(newBlock);
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Move toolbar and show plus button because new Block is empty
|
|
923
|
+
*/
|
|
924
|
+
this.Blok.Toolbar.moveAndOpen(newBlock);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* All clicks on document
|
|
932
|
+
* @param {MouseEvent} event - Click event
|
|
933
|
+
*/
|
|
934
|
+
private documentClicked(event: MouseEvent): void {
|
|
935
|
+
/**
|
|
936
|
+
* Sometimes we emulate click on some UI elements, for example by Enter on Block Settings button
|
|
937
|
+
* We don't need to handle such events, because they handled in other place.
|
|
938
|
+
*/
|
|
939
|
+
if (!event.isTrusted) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Close Inline Toolbar when nothing selected
|
|
944
|
+
* Do not fire check on clicks at the Inline Toolbar buttons
|
|
945
|
+
*/
|
|
946
|
+
const target = event.target as HTMLElement;
|
|
947
|
+
const clickedInsideOfBlok = this.nodes.holder.contains(target) || Selection.isAtBlok;
|
|
948
|
+
const clickedInsideRedactor = this.nodes.redactor.contains(target);
|
|
949
|
+
const clickedInsideToolbar = this.Blok.Toolbar.contains(target);
|
|
950
|
+
const clickedInsideInlineToolbar = this.Blok.InlineToolbar.containsNode(target);
|
|
951
|
+
const clickedInsideBlokSurface = clickedInsideOfBlok || clickedInsideToolbar;
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Check if click is on Block Settings, Settings Toggler, or Plus Button
|
|
955
|
+
* These elements have their own click handlers and should not trigger default behavior
|
|
956
|
+
*/
|
|
957
|
+
const isClickedInsideBlockSettings = this.Blok.BlockSettings.contains(target);
|
|
958
|
+
const isClickedInsideBlockSettingsToggler = this.Blok.Toolbar.nodes.settingsToggler?.contains(target);
|
|
959
|
+
const isClickedInsidePlusButton = this.Blok.Toolbar.nodes.plusButton?.contains(target);
|
|
960
|
+
const doNotProcess = isClickedInsideBlockSettings || isClickedInsideBlockSettingsToggler || isClickedInsidePlusButton;
|
|
961
|
+
|
|
962
|
+
const shouldClearCurrentBlock = !clickedInsideBlokSurface || (!clickedInsideRedactor && !clickedInsideToolbar);
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Don't clear current block when clicking on settings toggler, plus button, or inside block settings
|
|
966
|
+
* These elements need the current block to function properly
|
|
967
|
+
*/
|
|
968
|
+
if (shouldClearCurrentBlock && !doNotProcess) {
|
|
969
|
+
/**
|
|
970
|
+
* Clear pointer on BlockManager
|
|
971
|
+
*
|
|
972
|
+
* Current page might contain several instances
|
|
973
|
+
* Click between instances MUST clear focus, pointers and close toolbars
|
|
974
|
+
*/
|
|
975
|
+
this.Blok.BlockManager.unsetCurrentBlock();
|
|
976
|
+
this.Blok.Toolbar.close();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const shouldCloseBlockSettings = this.Blok.BlockSettings.opened && !doNotProcess;
|
|
980
|
+
if (shouldCloseBlockSettings) {
|
|
981
|
+
this.Blok.BlockSettings.close();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (shouldCloseBlockSettings && clickedInsideRedactor) {
|
|
985
|
+
const clickedBlock = this.Blok.BlockManager.getBlockByChildNode(target);
|
|
986
|
+
this.Blok.Toolbar.moveAndOpen(clickedBlock);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Clear Selection if user clicked somewhere
|
|
991
|
+
* But preserve selection when clicking on block settings toggler or inside block settings
|
|
992
|
+
* to allow multi-block operations like conversion
|
|
993
|
+
*/
|
|
994
|
+
if (!doNotProcess) {
|
|
995
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Close Inline Toolbar when clicking outside of it
|
|
1000
|
+
* This handles clicks anywhere outside the inline toolbar,
|
|
1001
|
+
* including inside the editor content area or on page controls
|
|
1002
|
+
*/
|
|
1003
|
+
if (this.Blok.InlineToolbar.opened && !clickedInsideInlineToolbar) {
|
|
1004
|
+
this.Blok.InlineToolbar.close();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* First touch on blok
|
|
1010
|
+
* Fired before click
|
|
1011
|
+
*
|
|
1012
|
+
* Used to change current block — we need to do it before 'selectionChange' event.
|
|
1013
|
+
* Also:
|
|
1014
|
+
* - Move and show the Toolbar
|
|
1015
|
+
* - Set a Caret
|
|
1016
|
+
* @param event - touch or mouse event
|
|
1017
|
+
*/
|
|
1018
|
+
private documentTouched(event: Event): void {
|
|
1019
|
+
const initialTarget = event.target as HTMLElement;
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* If click was fired on Blok`s wrapper, try to get clicked node by elementFromPoint method
|
|
1023
|
+
*/
|
|
1024
|
+
const clickedNode = (() => {
|
|
1025
|
+
if (initialTarget !== this.nodes.redactor) {
|
|
1026
|
+
return initialTarget;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (event instanceof MouseEvent) {
|
|
1030
|
+
const nodeFromPoint = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
|
|
1031
|
+
|
|
1032
|
+
return nodeFromPoint ?? initialTarget;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (event instanceof TouchEvent && event.touches.length > 0) {
|
|
1036
|
+
const { clientX, clientY } = event.touches[0];
|
|
1037
|
+
const nodeFromPoint = document.elementFromPoint(clientX, clientY) as HTMLElement | null;
|
|
1038
|
+
|
|
1039
|
+
return nodeFromPoint ?? initialTarget;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return initialTarget;
|
|
1043
|
+
})();
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Select clicked Block as Current
|
|
1047
|
+
*/
|
|
1048
|
+
try {
|
|
1049
|
+
this.Blok.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
|
1050
|
+
} catch (_e) {
|
|
1051
|
+
/**
|
|
1052
|
+
* If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block
|
|
1053
|
+
*/
|
|
1054
|
+
if (!this.Blok.RectangleSelection.isRectActivated()) {
|
|
1055
|
+
this.Blok.Caret.setToTheLastBlock();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Move and open toolbar
|
|
1061
|
+
* (used for showing Block Settings toggler after opening and closing Inline Toolbar)
|
|
1062
|
+
*/
|
|
1063
|
+
if (!this.Blok.ReadOnly.isEnabled && !this.Blok.Toolbar.contains(initialTarget)) {
|
|
1064
|
+
this.Blok.Toolbar.moveAndOpen(undefined, clickedNode);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* All clicks on the redactor zone
|
|
1070
|
+
* @param {MouseEvent} event - click event
|
|
1071
|
+
* @description
|
|
1072
|
+
* - By clicks on the Blok's bottom zone:
|
|
1073
|
+
* - if last Block is empty, set a Caret to this
|
|
1074
|
+
* - otherwise, add a new empty Block and set a Caret to that
|
|
1075
|
+
*/
|
|
1076
|
+
private redactorClicked(event: MouseEvent): void {
|
|
1077
|
+
if (!Selection.isCollapsed) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* case when user clicks on anchor element
|
|
1083
|
+
* if it is clicked via ctrl key, then we open new window with url
|
|
1084
|
+
*/
|
|
1085
|
+
const element = event.target as Element;
|
|
1086
|
+
const ctrlKey = event.metaKey || event.ctrlKey;
|
|
1087
|
+
const shouldOpenAnchorInNewTab = $.isAnchor(element) && ctrlKey;
|
|
1088
|
+
|
|
1089
|
+
if (!shouldOpenAnchorInNewTab) {
|
|
1090
|
+
this.processBottomZoneClick(event);
|
|
1091
|
+
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
event.stopImmediatePropagation();
|
|
1096
|
+
event.stopPropagation();
|
|
1097
|
+
|
|
1098
|
+
const href = element.getAttribute('href');
|
|
1099
|
+
|
|
1100
|
+
if (!href) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const validUrl = getValidUrl(href);
|
|
1105
|
+
|
|
1106
|
+
openTab(validUrl);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Check if user clicks on the Blok's bottom zone:
|
|
1111
|
+
* - set caret to the last block
|
|
1112
|
+
* - or add new empty block
|
|
1113
|
+
* @param event - click event
|
|
1114
|
+
*/
|
|
1115
|
+
private processBottomZoneClick(event: MouseEvent): void {
|
|
1116
|
+
const lastBlock = this.Blok.BlockManager.getBlockByIndex(-1);
|
|
1117
|
+
|
|
1118
|
+
const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom;
|
|
1119
|
+
const clickedCoord = event.pageY;
|
|
1120
|
+
const { BlockSelection } = this.Blok;
|
|
1121
|
+
const isClickedBottom = event.target instanceof Element &&
|
|
1122
|
+
event.target.isEqualNode(this.nodes.redactor) &&
|
|
1123
|
+
/**
|
|
1124
|
+
* If there is cross block selection started, target will be equal to redactor so we need additional check
|
|
1125
|
+
*/
|
|
1126
|
+
!BlockSelection.anyBlockSelected &&
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Prevent caret jumping (to last block) when clicking between blocks
|
|
1130
|
+
*/
|
|
1131
|
+
lastBlockBottomCoord < clickedCoord;
|
|
1132
|
+
|
|
1133
|
+
if (!isClickedBottom) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
event.stopImmediatePropagation();
|
|
1138
|
+
event.stopPropagation();
|
|
1139
|
+
|
|
1140
|
+
const { BlockManager, Caret, Toolbar } = this.Blok;
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Insert a default-block at the bottom if:
|
|
1144
|
+
* - last-block is not a default-block (Text)
|
|
1145
|
+
* to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table)
|
|
1146
|
+
* - Or, default-block is not empty
|
|
1147
|
+
*/
|
|
1148
|
+
if (!BlockManager.lastBlock?.tool.isDefault || !BlockManager.lastBlock?.isEmpty) {
|
|
1149
|
+
BlockManager.insertAtEnd();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Set the caret and toolbar to empty Block
|
|
1154
|
+
*/
|
|
1155
|
+
Caret.setToTheLastBlock();
|
|
1156
|
+
Toolbar.moveAndOpen(BlockManager.lastBlock);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Handle selection changes on mobile devices
|
|
1161
|
+
* Uses for showing the Inline Toolbar
|
|
1162
|
+
*/
|
|
1163
|
+
private selectionChanged(): void {
|
|
1164
|
+
const { CrossBlockSelection, BlockSelection } = this.Blok;
|
|
1165
|
+
const focusedElement = Selection.anchorElement;
|
|
1166
|
+
|
|
1167
|
+
if (CrossBlockSelection.isCrossBlockSelectionStarted && BlockSelection.anyBlockSelected) {
|
|
1168
|
+
// Removes all ranges when any Block is selected
|
|
1169
|
+
Selection.get()?.removeAllRanges();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Ignore transient selection changes triggered by fake background wrappers (used by inline tools
|
|
1174
|
+
* like Convert) while the Inline Toolbar is already open. Otherwise, the toolbar gets torn down
|
|
1175
|
+
* and re-rendered, which closes nested popovers before a user can click their items.
|
|
1176
|
+
*/
|
|
1177
|
+
const hasFakeBackground = document.querySelector('[data-blok-fake-background="true"]') !== null;
|
|
1178
|
+
|
|
1179
|
+
if (hasFakeBackground && this.Blok?.InlineToolbar?.opened) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Usual clicks on some controls, for example, Block Tunes Toggler
|
|
1185
|
+
*/
|
|
1186
|
+
if (!focusedElement && !Selection.range) {
|
|
1187
|
+
/**
|
|
1188
|
+
* If there is no selected range, close inline toolbar
|
|
1189
|
+
* @todo Make this method more straightforward
|
|
1190
|
+
*/
|
|
1191
|
+
this.Blok.InlineToolbar.close();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (!focusedElement) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Event can be fired on clicks at non-block-content elements,
|
|
1200
|
+
* for example, at the Inline Toolbar or some Block Tune element.
|
|
1201
|
+
* We also make sure that the closest block belongs to the current blok and not a parent
|
|
1202
|
+
*/
|
|
1203
|
+
const closestBlock = focusedElement.closest('[data-blok-testid="block-content"]');
|
|
1204
|
+
const clickedOutsideBlockContent = closestBlock === null || (closestBlock.closest('[data-blok-testid="blok-editor"]') !== this.nodes.wrapper);
|
|
1205
|
+
|
|
1206
|
+
const inlineToolbarEnabledForExternalTool = (focusedElement as HTMLElement).getAttribute('data-blok-inline-toolbar') === 'true';
|
|
1207
|
+
const shouldCloseInlineToolbar = clickedOutsideBlockContent && !this.Blok.InlineToolbar.containsNode(focusedElement);
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* If the inline toolbar is already open without a nested popover,
|
|
1211
|
+
* don't close or re-render it. This prevents the toolbar from flickering
|
|
1212
|
+
* when the user closes a nested popover (e.g., via Esc key).
|
|
1213
|
+
*
|
|
1214
|
+
* However, if the selection is now collapsed or empty (e.g., user deleted the selected text),
|
|
1215
|
+
* we should close the inline toolbar since there's nothing to format.
|
|
1216
|
+
*
|
|
1217
|
+
* Important: Don't close the toolbar if a flipper item is focused (user is navigating
|
|
1218
|
+
* with Tab/Arrow keys). In some browsers (webkit), keyboard navigation within the
|
|
1219
|
+
* popover can trigger selectionchange events that make the selection appear empty.
|
|
1220
|
+
*/
|
|
1221
|
+
const currentSelection = Selection.get();
|
|
1222
|
+
const selectionIsEmpty = !currentSelection || currentSelection.isCollapsed || Selection.text.length === 0;
|
|
1223
|
+
const hasFlipperFocus = this.Blok.InlineToolbar.hasFlipperFocus;
|
|
1224
|
+
|
|
1225
|
+
if (selectionIsEmpty && this.Blok.InlineToolbar.opened && !hasFlipperFocus) {
|
|
1226
|
+
this.Blok.InlineToolbar.close();
|
|
1227
|
+
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (this.Blok.InlineToolbar.opened && !this.Blok.InlineToolbar.hasNestedPopoverOpen) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (shouldCloseInlineToolbar) {
|
|
1236
|
+
/**
|
|
1237
|
+
* If new selection is not on Inline Toolbar, we need to close it
|
|
1238
|
+
*/
|
|
1239
|
+
this.Blok.InlineToolbar.close();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (clickedOutsideBlockContent && !inlineToolbarEnabledForExternalTool) {
|
|
1243
|
+
/**
|
|
1244
|
+
* Case when we click on external tool elements,
|
|
1245
|
+
* for example some Block Tune element.
|
|
1246
|
+
* If this external content editable element has data-inline-toolbar="true"
|
|
1247
|
+
*/
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Set current block when entering to Blok by tab key
|
|
1253
|
+
*/
|
|
1254
|
+
if (!this.Blok.BlockManager.currentBlock) {
|
|
1255
|
+
this.Blok.BlockManager.setCurrentBlockByChildNode(focusedElement);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
void this.Blok.InlineToolbar.tryToShow(true);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Blok provides and ability to show placeholders for empty contenteditable elements
|
|
1263
|
+
*
|
|
1264
|
+
* This method watches for input and focus events and toggles 'data-blok-empty' attribute
|
|
1265
|
+
* to workaroud the case, when inputs contains only <br>s and has no visible content
|
|
1266
|
+
* Then, CSS could rely on this attribute to show placeholders
|
|
1267
|
+
*/
|
|
1268
|
+
private enableInputsEmptyMark(): void {
|
|
1269
|
+
/**
|
|
1270
|
+
* Toggle data-blok-empty attribute on input depending on its emptiness
|
|
1271
|
+
* @param event - input or focus event
|
|
1272
|
+
*/
|
|
1273
|
+
const handleInputOrFocusChange = (event: Event): void => {
|
|
1274
|
+
const input = event.target as HTMLElement;
|
|
1275
|
+
|
|
1276
|
+
toggleEmptyMark(input);
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'input', handleInputOrFocusChange);
|
|
1280
|
+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusin', handleInputOrFocusChange);
|
|
1281
|
+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusout', handleInputOrFocusChange);
|
|
1282
|
+
}
|
|
1283
|
+
}
|