@jackuait/blok 0.4.1-beta.1 → 0.4.1-beta.11
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-oNSQ3HA6.mjs +13217 -0
- package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
- package/dist/chunks/i18next-loader-BdNRw4n4.mjs +43 -0
- package/dist/{index-CEXLTV6f.mjs → chunks/index-DHgXmfki.mjs} +2 -2
- package/dist/chunks/inline-tool-convert-CRqgjRim.mjs +1989 -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 +3117 -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 +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 +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 +781 -0
- package/src/components/modules/toolbar/index.ts +1315 -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 +601 -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 +186 -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 +676 -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 +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 +646 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/list/index.ts +1819 -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/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 +29 -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 +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 +92 -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 +18 -5
- package/types/utils/popover/popover.d.ts +7 -0
- package/dist/blok-C8XbyLHh.mjs +0 -25795
- package/dist/blok.umd.js +0 -181
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contains keyboard and mouse events bound on each Block by Block Manager
|
|
3
|
+
*/
|
|
4
|
+
import { Module } from '../__module';
|
|
5
|
+
import { delay, isIosDevice, keyCodes } from '../utils';
|
|
6
|
+
import { SelectionUtils } from '../selection';
|
|
7
|
+
import { Flipper } from '../flipper';
|
|
8
|
+
import type { Block } from '../block';
|
|
9
|
+
import { areBlocksMergeable } from '../utils/blocks';
|
|
10
|
+
import { findNbspAfterEmptyInline, focus, isCaretAtEndOfInput, isCaretAtStartOfInput } from '../utils/caret';
|
|
11
|
+
|
|
12
|
+
const KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP: Record<string, number> = {
|
|
13
|
+
Backspace: keyCodes.BACKSPACE,
|
|
14
|
+
Delete: keyCodes.DELETE,
|
|
15
|
+
Enter: keyCodes.ENTER,
|
|
16
|
+
Tab: keyCodes.TAB,
|
|
17
|
+
ArrowDown: keyCodes.DOWN,
|
|
18
|
+
ArrowRight: keyCodes.RIGHT,
|
|
19
|
+
ArrowUp: keyCodes.UP,
|
|
20
|
+
ArrowLeft: keyCodes.LEFT,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const PRINTABLE_SPECIAL_KEYS = new Set(['Enter', 'Process', 'Spacebar', 'Space', 'Dead']);
|
|
24
|
+
const EDITABLE_INPUT_SELECTOR = '[contenteditable="true"], textarea, input';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if the keyboard event is a block movement shortcut (Cmd/Ctrl+Shift+Arrow)
|
|
28
|
+
* @param event - keyboard event
|
|
29
|
+
* @param direction - 'up' or 'down'
|
|
30
|
+
* @returns true if this is a block movement shortcut
|
|
31
|
+
*/
|
|
32
|
+
const isBlockMovementShortcut = (event: KeyboardEvent, direction: 'up' | 'down'): boolean => {
|
|
33
|
+
const targetKey = direction === 'up' ? 'ArrowUp' : 'ArrowDown';
|
|
34
|
+
|
|
35
|
+
return event.key === targetKey &&
|
|
36
|
+
event.shiftKey &&
|
|
37
|
+
(event.ctrlKey || event.metaKey);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
*
|
|
42
|
+
*/
|
|
43
|
+
export class BlockEvents extends Module {
|
|
44
|
+
/**
|
|
45
|
+
* Tool name for list items
|
|
46
|
+
*/
|
|
47
|
+
private static readonly LIST_TOOL_NAME = 'list';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tool name for headers
|
|
51
|
+
*/
|
|
52
|
+
private static readonly HEADER_TOOL_NAME = 'header';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the depth of a list block from its data attribute.
|
|
56
|
+
* @param block - the block to get depth from
|
|
57
|
+
* @returns depth value (0 if not found or not a list)
|
|
58
|
+
*/
|
|
59
|
+
private getListBlockDepth(block: Block): number {
|
|
60
|
+
const depthAttr = block.holder?.querySelector('[data-list-depth]')?.getAttribute('data-list-depth');
|
|
61
|
+
|
|
62
|
+
return depthAttr ? parseInt(depthAttr, 10) : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if all selected list items can be indented.
|
|
67
|
+
* Each item must have a previous list item, and its depth must be <= previous item's depth.
|
|
68
|
+
* @returns true if all selected items can be indented
|
|
69
|
+
*/
|
|
70
|
+
private canIndentSelectedListItems(): boolean {
|
|
71
|
+
const { BlockSelection, BlockManager } = this.Blok;
|
|
72
|
+
|
|
73
|
+
for (const block of BlockSelection.selectedBlocks) {
|
|
74
|
+
const blockIndex = BlockManager.getBlockIndex(block);
|
|
75
|
+
|
|
76
|
+
if (blockIndex === undefined || blockIndex === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const previousBlock = BlockManager.getBlockByIndex(blockIndex - 1);
|
|
81
|
+
|
|
82
|
+
if (!previousBlock || previousBlock.name !== BlockEvents.LIST_TOOL_NAME) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.getListBlockDepth(block) > this.getListBlockDepth(previousBlock)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if all selected list items can be outdented (all have depth > 0).
|
|
96
|
+
* @returns true if all selected items can be outdented
|
|
97
|
+
*/
|
|
98
|
+
private canOutdentSelectedListItems(): boolean {
|
|
99
|
+
return this.Blok.BlockSelection.selectedBlocks.every((block) => this.getListBlockDepth(block) > 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update depth of all selected list items.
|
|
104
|
+
* @param delta - depth change (+1 for indent, -1 for outdent)
|
|
105
|
+
*/
|
|
106
|
+
private async updateSelectedListItemsDepth(delta: number): Promise<void> {
|
|
107
|
+
const { BlockSelection, BlockManager } = this.Blok;
|
|
108
|
+
|
|
109
|
+
const blockIndices = BlockSelection.selectedBlocks
|
|
110
|
+
.map((block) => BlockManager.getBlockIndex(block))
|
|
111
|
+
.filter((index): index is number => index >= 0)
|
|
112
|
+
.sort((a, b) => a - b);
|
|
113
|
+
|
|
114
|
+
for (const blockIndex of blockIndices) {
|
|
115
|
+
const block = BlockManager.getBlockByIndex(blockIndex);
|
|
116
|
+
|
|
117
|
+
if (!block) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const savedData = await block.save();
|
|
122
|
+
const newBlock = await BlockManager.update(block, {
|
|
123
|
+
...savedData,
|
|
124
|
+
depth: Math.max(0, this.getListBlockDepth(block) + delta),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
newBlock.selected = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
BlockSelection.clearCache();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handles Tab/Shift+Tab for multi-selected list items.
|
|
135
|
+
* @param event - keyboard event
|
|
136
|
+
* @returns true if the event was handled, false to fall through to default behavior
|
|
137
|
+
*/
|
|
138
|
+
private handleSelectedBlocksIndent(event: KeyboardEvent): boolean {
|
|
139
|
+
const { BlockSelection } = this.Blok;
|
|
140
|
+
|
|
141
|
+
if (!BlockSelection.anyBlockSelected) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const allListItems = BlockSelection.selectedBlocks.every(
|
|
146
|
+
(block) => block.name === BlockEvents.LIST_TOOL_NAME
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!allListItems) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
|
|
155
|
+
const isOutdent = event.shiftKey;
|
|
156
|
+
|
|
157
|
+
if (isOutdent && this.canOutdentSelectedListItems()) {
|
|
158
|
+
void this.updateSelectedListItemsDepth(-1);
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!isOutdent && this.canIndentSelectedListItems()) {
|
|
164
|
+
void this.updateSelectedListItemsDepth(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* All keydowns on Block
|
|
172
|
+
* @param {KeyboardEvent} event - keydown
|
|
173
|
+
*/
|
|
174
|
+
public keydown(event: KeyboardEvent): void {
|
|
175
|
+
/**
|
|
176
|
+
* Handle navigation mode keys first
|
|
177
|
+
*/
|
|
178
|
+
if (this.handleNavigationModeKeys(event)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle Escape key to enable navigation mode
|
|
184
|
+
*/
|
|
185
|
+
if (event.key === 'Escape') {
|
|
186
|
+
this.handleEscapeToEnableNavigation(event);
|
|
187
|
+
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Run common method for all keydown events
|
|
193
|
+
*/
|
|
194
|
+
this.beforeKeydownProcessing(event);
|
|
195
|
+
|
|
196
|
+
if (this.handleSelectedBlocksDeletion(event)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* If event was already handled by something (e.g. tool), we should not handle it
|
|
202
|
+
*/
|
|
203
|
+
if (event.defaultPrevented) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const keyCode = this.getKeyCode(event);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fire keydown processor by normalized keyboard code
|
|
211
|
+
*/
|
|
212
|
+
switch (keyCode) {
|
|
213
|
+
case keyCodes.BACKSPACE:
|
|
214
|
+
this.backspace(event);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case keyCodes.DELETE:
|
|
218
|
+
this.delete(event);
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case keyCodes.ENTER:
|
|
222
|
+
this.enter(event);
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case keyCodes.DOWN:
|
|
226
|
+
case keyCodes.RIGHT:
|
|
227
|
+
this.arrowRightAndDown(event);
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case keyCodes.UP:
|
|
231
|
+
case keyCodes.LEFT:
|
|
232
|
+
this.arrowLeftAndUp(event);
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case keyCodes.TAB:
|
|
236
|
+
if (this.handleSelectedBlocksIndent(event)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.tabPressed(event);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc
|
|
245
|
+
* @todo probably using "beforeInput" event would be better here
|
|
246
|
+
*/
|
|
247
|
+
if (event.key === '/' && !event.ctrlKey && !event.metaKey) {
|
|
248
|
+
this.slashPressed(event);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings
|
|
253
|
+
* We check for "code" here since on different keyboard layouts there can be different keys in place of Slash.
|
|
254
|
+
*/
|
|
255
|
+
if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
this.commandSlashPressed();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Tries to delete selected blocks when remove keys pressed.
|
|
263
|
+
* @param event - keyboard event
|
|
264
|
+
* @returns true if event was handled
|
|
265
|
+
*/
|
|
266
|
+
private handleSelectedBlocksDeletion(event: KeyboardEvent): boolean {
|
|
267
|
+
const { BlockSelection, BlockManager, Caret, BlockSettings } = this.Blok;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Ignore delete/backspace from inside the BlockSettings popover (e.g., search input)
|
|
271
|
+
*/
|
|
272
|
+
if (BlockSettings.contains(event.target as HTMLElement)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const isRemoveKey = event.key === 'Backspace' || event.key === 'Delete';
|
|
277
|
+
const selectionExists = SelectionUtils.isSelectionExists;
|
|
278
|
+
const selectionCollapsed = SelectionUtils.isCollapsed === true;
|
|
279
|
+
const shouldHandleSelectionDeletion = isRemoveKey &&
|
|
280
|
+
BlockSelection.anyBlockSelected &&
|
|
281
|
+
(!selectionExists || selectionCollapsed);
|
|
282
|
+
|
|
283
|
+
if (!shouldHandleSelectionDeletion) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
|
288
|
+
|
|
289
|
+
if (selectionPositionIndex !== undefined) {
|
|
290
|
+
const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
|
|
291
|
+
|
|
292
|
+
Caret.setToBlock(insertedBlock, Caret.positions.START);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
BlockSelection.clearSelection(event);
|
|
296
|
+
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
event.stopImmediatePropagation();
|
|
299
|
+
event.stopPropagation();
|
|
300
|
+
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Handles Escape key press to enable navigation mode.
|
|
306
|
+
* Called when user presses Escape while editing a block.
|
|
307
|
+
* @param event - keyboard event
|
|
308
|
+
*/
|
|
309
|
+
private handleEscapeToEnableNavigation(event: KeyboardEvent): void {
|
|
310
|
+
const { BlockSelection, BlockSettings, InlineToolbar, Toolbar } = this.Blok;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* If any toolbar is open, let the UI module handle closing it
|
|
314
|
+
*/
|
|
315
|
+
if (BlockSettings.opened || InlineToolbar.opened || Toolbar.toolbox.opened) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* If blocks are selected, let the UI module handle clearing selection
|
|
321
|
+
*/
|
|
322
|
+
if (BlockSelection.anyBlockSelected) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Enable navigation mode
|
|
328
|
+
*/
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
Toolbar.close();
|
|
331
|
+
BlockSelection.enableNavigationMode();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handles keyboard events when navigation mode is active.
|
|
336
|
+
* In navigation mode:
|
|
337
|
+
* - ArrowUp/ArrowDown: navigate between blocks
|
|
338
|
+
* - Enter: exit navigation mode and focus the block for editing
|
|
339
|
+
* - Escape: exit navigation mode without focusing
|
|
340
|
+
* @param event - keyboard event
|
|
341
|
+
* @returns true if event was handled
|
|
342
|
+
*/
|
|
343
|
+
private handleNavigationModeKeys(event: KeyboardEvent): boolean {
|
|
344
|
+
const { BlockSelection } = this.Blok;
|
|
345
|
+
|
|
346
|
+
if (!BlockSelection.navigationModeEnabled) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const key = event.key;
|
|
351
|
+
|
|
352
|
+
switch (key) {
|
|
353
|
+
case 'ArrowDown':
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
event.stopPropagation();
|
|
356
|
+
BlockSelection.navigateNext();
|
|
357
|
+
|
|
358
|
+
return true;
|
|
359
|
+
|
|
360
|
+
case 'ArrowUp':
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
event.stopPropagation();
|
|
363
|
+
BlockSelection.navigatePrevious();
|
|
364
|
+
|
|
365
|
+
return true;
|
|
366
|
+
|
|
367
|
+
case 'Enter':
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
event.stopPropagation();
|
|
370
|
+
event.stopImmediatePropagation();
|
|
371
|
+
BlockSelection.disableNavigationMode(true);
|
|
372
|
+
|
|
373
|
+
return true;
|
|
374
|
+
|
|
375
|
+
case 'Escape':
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
event.stopPropagation();
|
|
378
|
+
BlockSelection.disableNavigationMode(false);
|
|
379
|
+
|
|
380
|
+
return true;
|
|
381
|
+
|
|
382
|
+
default:
|
|
383
|
+
/**
|
|
384
|
+
* Any other key exits navigation mode and allows normal input
|
|
385
|
+
*/
|
|
386
|
+
if (this.isPrintableKeyEvent(event)) {
|
|
387
|
+
BlockSelection.disableNavigationMode(true);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Fires on keydown before event processing
|
|
396
|
+
* @param {KeyboardEvent} event - keydown
|
|
397
|
+
*/
|
|
398
|
+
public beforeKeydownProcessing(event: KeyboardEvent): void {
|
|
399
|
+
/**
|
|
400
|
+
* Do not close Toolbox on Tabs or on Enter with opened Toolbox
|
|
401
|
+
*/
|
|
402
|
+
if (!this.needToolbarClosing(event)) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* When user type something:
|
|
408
|
+
* - close Toolbar
|
|
409
|
+
* - clear block highlighting
|
|
410
|
+
*/
|
|
411
|
+
if (!this.isPrintableKeyEvent(event)) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.Blok.Toolbar.close();
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Allow to use shortcuts with selected blocks
|
|
419
|
+
* @type {boolean}
|
|
420
|
+
*/
|
|
421
|
+
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
|
|
422
|
+
|
|
423
|
+
if (isShortcut) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Key up on Block:
|
|
432
|
+
* - shows Inline Toolbar if something selected
|
|
433
|
+
* - shows conversion toolbar with 85% of block selection
|
|
434
|
+
* @param {KeyboardEvent} event - keyup event
|
|
435
|
+
*/
|
|
436
|
+
public keyup(event: KeyboardEvent): void {
|
|
437
|
+
/**
|
|
438
|
+
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
|
|
439
|
+
*/
|
|
440
|
+
if (event.shiftKey) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Check if blok is empty on each keyup and add special css class to wrapper
|
|
446
|
+
*/
|
|
447
|
+
this.Blok.UI.checkEmptiness();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Regex patterns for detecting list shortcuts.
|
|
452
|
+
* Matches patterns like "1. ", "1) ", "2. ", etc. at the start of text
|
|
453
|
+
* Captures remaining content after the shortcut in group 2
|
|
454
|
+
*/
|
|
455
|
+
private static readonly ORDERED_LIST_PATTERN = /^(\d+)[.)]\s([\s\S]*)$/;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Regex pattern for detecting checklist shortcuts.
|
|
459
|
+
* Matches patterns like "[] ", "[ ] ", "[x] ", "[X] " at the start of text
|
|
460
|
+
* Captures remaining content after the shortcut in group 2
|
|
461
|
+
*/
|
|
462
|
+
private static readonly CHECKLIST_PATTERN = /^\[(x|X| )?\]\s([\s\S]*)$/;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Regex pattern for detecting bulleted list shortcuts.
|
|
466
|
+
* Matches patterns like "- " or "* " at the start of text
|
|
467
|
+
* Captures remaining content after the shortcut in group 1
|
|
468
|
+
*/
|
|
469
|
+
private static readonly UNORDERED_LIST_PATTERN = /^[-*]\s([\s\S]*)$/;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Regex pattern for detecting header shortcuts.
|
|
473
|
+
* Matches patterns like "# ", "## ", "### " etc. at the start of text (1-6 hashes)
|
|
474
|
+
* Captures remaining content after the shortcut in group 2
|
|
475
|
+
*/
|
|
476
|
+
private static readonly HEADER_PATTERN = /^(#{1,6})\s([\s\S]*)$/;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Input event handler for Block
|
|
480
|
+
* Detects markdown-like shortcuts for auto-converting to lists or headers
|
|
481
|
+
* @param {InputEvent} event - input event
|
|
482
|
+
*/
|
|
483
|
+
public input(event: InputEvent): void {
|
|
484
|
+
/**
|
|
485
|
+
* Only handle insertText events (typing) that end with a space
|
|
486
|
+
*/
|
|
487
|
+
if (event.inputType !== 'insertText' || event.data !== ' ') {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.handleListShortcut();
|
|
492
|
+
this.handleHeaderShortcut();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Check if current block content matches a list shortcut pattern
|
|
497
|
+
* and convert to appropriate list type.
|
|
498
|
+
* Supports conversion even when there's existing text after the shortcut.
|
|
499
|
+
* Preserves HTML content and maintains caret position.
|
|
500
|
+
*/
|
|
501
|
+
private handleListShortcut(): void {
|
|
502
|
+
const { BlockManager, Tools } = this.Blok;
|
|
503
|
+
const currentBlock = BlockManager.currentBlock;
|
|
504
|
+
|
|
505
|
+
if (!currentBlock) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Only convert default blocks (paragraphs)
|
|
511
|
+
*/
|
|
512
|
+
if (!currentBlock.tool.isDefault) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Check if list tool is available
|
|
518
|
+
*/
|
|
519
|
+
const listTool = Tools.blockTools.get('list');
|
|
520
|
+
|
|
521
|
+
if (!listTool) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const currentInput = currentBlock.currentInput;
|
|
526
|
+
|
|
527
|
+
if (!currentInput) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Use textContent to match the shortcut pattern
|
|
533
|
+
*/
|
|
534
|
+
const textContent = currentInput.textContent || '';
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get the depth from the block holder if it was previously a nested list item
|
|
538
|
+
* This preserves nesting when converting back to a list
|
|
539
|
+
*/
|
|
540
|
+
const depthAttr = currentBlock.holder.getAttribute('data-blok-depth');
|
|
541
|
+
const depth = depthAttr ? parseInt(depthAttr, 10) : 0;
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Check for checklist pattern (e.g., "[] ", "[ ] ", "[x] ", "[X] ")
|
|
545
|
+
*/
|
|
546
|
+
const checklistMatch = BlockEvents.CHECKLIST_PATTERN.exec(textContent);
|
|
547
|
+
|
|
548
|
+
if (checklistMatch) {
|
|
549
|
+
/**
|
|
550
|
+
* Determine if the checkbox should be checked
|
|
551
|
+
* [x] or [X] means checked, [] or [ ] means unchecked
|
|
552
|
+
*/
|
|
553
|
+
const isChecked = checklistMatch[1]?.toLowerCase() === 'x';
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Extract remaining content (group 2) and calculate shortcut length
|
|
557
|
+
* Shortcut length: "[" + optional char + "]" + " " = 3 or 4 chars
|
|
558
|
+
*/
|
|
559
|
+
const shortcutLength = checklistMatch[1] !== undefined ? 4 : 3;
|
|
560
|
+
const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
|
|
561
|
+
const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
|
|
562
|
+
|
|
563
|
+
const newBlock = BlockManager.replace(currentBlock, 'list', {
|
|
564
|
+
text: remainingHtml,
|
|
565
|
+
style: 'checklist',
|
|
566
|
+
checked: isChecked,
|
|
567
|
+
...(depth > 0 ? { depth } : {}),
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
this.setCaretAfterConversion(newBlock, caretOffset);
|
|
571
|
+
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Check for unordered/bulleted list pattern (e.g., "- " or "* ")
|
|
577
|
+
*/
|
|
578
|
+
const unorderedMatch = BlockEvents.UNORDERED_LIST_PATTERN.exec(textContent);
|
|
579
|
+
|
|
580
|
+
if (unorderedMatch) {
|
|
581
|
+
/**
|
|
582
|
+
* Extract remaining content (group 1) and calculate shortcut length
|
|
583
|
+
* Shortcut length: "-" or "*" + " " = 2 chars
|
|
584
|
+
*/
|
|
585
|
+
const shortcutLength = 2;
|
|
586
|
+
const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
|
|
587
|
+
const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
|
|
588
|
+
|
|
589
|
+
const newBlock = BlockManager.replace(currentBlock, 'list', {
|
|
590
|
+
text: remainingHtml,
|
|
591
|
+
style: 'unordered',
|
|
592
|
+
checked: false,
|
|
593
|
+
...(depth > 0 ? { depth } : {}),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
this.setCaretAfterConversion(newBlock, caretOffset);
|
|
597
|
+
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Check for ordered list pattern (e.g., "1. " or "1) ")
|
|
603
|
+
*/
|
|
604
|
+
const orderedMatch = BlockEvents.ORDERED_LIST_PATTERN.exec(textContent);
|
|
605
|
+
|
|
606
|
+
if (!orderedMatch) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Extract the starting number from the pattern
|
|
612
|
+
*/
|
|
613
|
+
const startNumber = parseInt(orderedMatch[1], 10);
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Extract remaining content (group 2) and calculate shortcut length
|
|
617
|
+
* Shortcut length: number digits + "." or ")" + " " = orderedMatch[1].length + 2
|
|
618
|
+
*/
|
|
619
|
+
const shortcutLength = orderedMatch[1].length + 2;
|
|
620
|
+
const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
|
|
621
|
+
const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Convert to ordered list with the captured start number
|
|
625
|
+
*/
|
|
626
|
+
const listData: { text: string; style: string; checked: boolean; start?: number; depth?: number } = {
|
|
627
|
+
text: remainingHtml,
|
|
628
|
+
style: 'ordered',
|
|
629
|
+
checked: false,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Only include start if it's not 1 (the default)
|
|
633
|
+
if (startNumber !== 1) {
|
|
634
|
+
listData.start = startNumber;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Preserve depth if the block was previously nested
|
|
638
|
+
if (depth > 0) {
|
|
639
|
+
listData.depth = depth;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const newBlock = BlockManager.replace(currentBlock, 'list', listData);
|
|
643
|
+
|
|
644
|
+
this.setCaretAfterConversion(newBlock, caretOffset);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Check if current block matches a header shortcut pattern and convert it.
|
|
649
|
+
*/
|
|
650
|
+
private handleHeaderShortcut(): void {
|
|
651
|
+
const { BlockManager, Tools } = this.Blok;
|
|
652
|
+
const currentBlock = BlockManager.currentBlock;
|
|
653
|
+
|
|
654
|
+
if (!currentBlock?.tool.isDefault) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const headerTool = Tools.blockTools.get(BlockEvents.HEADER_TOOL_NAME);
|
|
659
|
+
|
|
660
|
+
if (!headerTool) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const currentInput = currentBlock.currentInput;
|
|
665
|
+
|
|
666
|
+
if (!currentInput) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const textContent = currentInput.textContent || '';
|
|
671
|
+
const { levels, shortcuts } = headerTool.settings as { levels?: number[]; shortcuts?: Record<number, string> };
|
|
672
|
+
const match = shortcuts === undefined
|
|
673
|
+
? this.matchDefaultHeaderShortcut(textContent)
|
|
674
|
+
: this.matchCustomHeaderShortcut(textContent, shortcuts);
|
|
675
|
+
|
|
676
|
+
if (!match || (levels && !levels.includes(match.level))) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const remainingHtml = this.extractRemainingHtml(currentInput, match.shortcutLength);
|
|
681
|
+
const caretOffset = this.getCaretOffset(currentInput) - match.shortcutLength;
|
|
682
|
+
|
|
683
|
+
const newBlock = BlockManager.replace(currentBlock, BlockEvents.HEADER_TOOL_NAME, {
|
|
684
|
+
text: remainingHtml,
|
|
685
|
+
level: match.level,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
this.setCaretAfterConversion(newBlock, caretOffset);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private matchDefaultHeaderShortcut(text: string): { level: number; shortcutLength: number } | null {
|
|
692
|
+
const match = BlockEvents.HEADER_PATTERN.exec(text);
|
|
693
|
+
|
|
694
|
+
return match ? { level: match[1].length, shortcutLength: match[1].length + 1 } : null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private matchCustomHeaderShortcut(
|
|
698
|
+
text: string,
|
|
699
|
+
shortcuts: Record<number, string>
|
|
700
|
+
): { level: number; shortcutLength: number } | null {
|
|
701
|
+
// Sort by prefix length descending to match longer prefixes first (e.g., "!!" before "!")
|
|
702
|
+
for (const [levelStr, prefix] of Object.entries(shortcuts).sort((a, b) => b[1].length - a[1].length)) {
|
|
703
|
+
if (text.length <= prefix.length || !text.startsWith(prefix)) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const charAfterPrefix = text.charCodeAt(prefix.length);
|
|
708
|
+
|
|
709
|
+
// 32 = regular space, 160 = non-breaking space (contenteditable uses nbsp)
|
|
710
|
+
if (charAfterPrefix === 32 || charAfterPrefix === 160) {
|
|
711
|
+
return { level: parseInt(levelStr, 10), shortcutLength: prefix.length + 1 };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Extract HTML content after a shortcut prefix
|
|
720
|
+
* @param input - the input element
|
|
721
|
+
* @param shortcutLength - length of the shortcut in text characters
|
|
722
|
+
* @returns HTML string with the content after the shortcut
|
|
723
|
+
*/
|
|
724
|
+
private extractRemainingHtml(input: HTMLElement, shortcutLength: number): string {
|
|
725
|
+
const innerHTML = input.innerHTML || '';
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Create a temporary element to manipulate the HTML
|
|
729
|
+
*/
|
|
730
|
+
const temp = document.createElement('div');
|
|
731
|
+
|
|
732
|
+
temp.innerHTML = innerHTML;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Walk through text nodes and collect nodes to modify
|
|
736
|
+
*/
|
|
737
|
+
const walker = document.createTreeWalker(temp, NodeFilter.SHOW_TEXT, null);
|
|
738
|
+
const nodesToModify = this.collectNodesToModify(walker, shortcutLength);
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Apply modifications
|
|
742
|
+
*/
|
|
743
|
+
for (const { node, removeCount } of nodesToModify) {
|
|
744
|
+
const text = node.textContent || '';
|
|
745
|
+
|
|
746
|
+
if (removeCount >= text.length) {
|
|
747
|
+
node.remove();
|
|
748
|
+
} else {
|
|
749
|
+
node.textContent = text.slice(removeCount);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return temp.innerHTML;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Collect text nodes that need modification to remove shortcut characters
|
|
758
|
+
* @param walker - TreeWalker for text nodes
|
|
759
|
+
* @param charsToRemove - total characters to remove
|
|
760
|
+
* @returns array of nodes with their removal counts
|
|
761
|
+
*/
|
|
762
|
+
private collectNodesToModify(
|
|
763
|
+
walker: TreeWalker,
|
|
764
|
+
charsToRemove: number
|
|
765
|
+
): Array<{ node: Text; removeCount: number }> {
|
|
766
|
+
const result: Array<{ node: Text; removeCount: number }> = [];
|
|
767
|
+
|
|
768
|
+
if (charsToRemove <= 0 || !walker.nextNode()) {
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const textNode = walker.currentNode as Text;
|
|
773
|
+
const nodeLength = textNode.textContent?.length || 0;
|
|
774
|
+
|
|
775
|
+
if (nodeLength <= charsToRemove) {
|
|
776
|
+
result.push({ node: textNode, removeCount: nodeLength });
|
|
777
|
+
|
|
778
|
+
return result.concat(this.collectNodesToModify(walker, charsToRemove - nodeLength));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
result.push({ node: textNode, removeCount: charsToRemove });
|
|
782
|
+
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Get the current caret offset within the input element
|
|
788
|
+
* @param input - the input element
|
|
789
|
+
* @returns offset in text characters from the start
|
|
790
|
+
*/
|
|
791
|
+
private getCaretOffset(input: HTMLElement): number {
|
|
792
|
+
const selection = window.getSelection();
|
|
793
|
+
|
|
794
|
+
if (!selection || selection.rangeCount === 0) {
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const range = selection.getRangeAt(0);
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Create a range from start of input to current caret position
|
|
802
|
+
*/
|
|
803
|
+
const preCaretRange = document.createRange();
|
|
804
|
+
|
|
805
|
+
preCaretRange.selectNodeContents(input);
|
|
806
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get the text length up to the caret
|
|
810
|
+
*/
|
|
811
|
+
return preCaretRange.toString().length;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Set caret position in the new block after conversion
|
|
816
|
+
* @param block - the new block
|
|
817
|
+
* @param offset - desired caret offset in text characters
|
|
818
|
+
*/
|
|
819
|
+
private setCaretAfterConversion(block: Block, offset: number): void {
|
|
820
|
+
const { Caret } = this.Blok;
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* If offset is 0 or negative, set to start
|
|
824
|
+
*/
|
|
825
|
+
if (offset <= 0) {
|
|
826
|
+
Caret.setToBlock(block, Caret.positions.START);
|
|
827
|
+
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Set caret to the specific offset
|
|
833
|
+
*/
|
|
834
|
+
Caret.setToBlock(block, Caret.positions.DEFAULT, offset);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Copying selected blocks
|
|
839
|
+
* Before putting to the clipboard we sanitize all blocks and then copy to the clipboard
|
|
840
|
+
* @param {ClipboardEvent} event - clipboard event
|
|
841
|
+
*/
|
|
842
|
+
public handleCommandC(event: ClipboardEvent): void {
|
|
843
|
+
const { BlockSelection } = this.Blok;
|
|
844
|
+
|
|
845
|
+
if (!BlockSelection.anyBlockSelected) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Copy Selected Blocks
|
|
850
|
+
void BlockSelection.copySelectedBlocks(event);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Copy and Delete selected Blocks
|
|
855
|
+
* @param {ClipboardEvent} event - clipboard event
|
|
856
|
+
*/
|
|
857
|
+
public handleCommandX(event: ClipboardEvent): void {
|
|
858
|
+
const { BlockSelection, BlockManager, Caret } = this.Blok;
|
|
859
|
+
|
|
860
|
+
if (!BlockSelection.anyBlockSelected) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
BlockSelection.copySelectedBlocks(event).then(() => {
|
|
865
|
+
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Insert default block in place of removed ones
|
|
869
|
+
*/
|
|
870
|
+
if (selectionPositionIndex !== undefined) {
|
|
871
|
+
const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
|
|
872
|
+
|
|
873
|
+
Caret.setToBlock(insertedBlock, Caret.positions.START);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Clear selection */
|
|
877
|
+
BlockSelection.clearSelection(event);
|
|
878
|
+
})
|
|
879
|
+
.catch(() => {
|
|
880
|
+
// Handle copy operation failure silently
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Tab pressed inside a Block.
|
|
886
|
+
* @param {KeyboardEvent} event - keydown
|
|
887
|
+
*/
|
|
888
|
+
private tabPressed(event: KeyboardEvent): void {
|
|
889
|
+
const { InlineToolbar, Caret } = this.Blok;
|
|
890
|
+
|
|
891
|
+
const isFlipperActivated = InlineToolbar.opened;
|
|
892
|
+
|
|
893
|
+
if (isFlipperActivated) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
|
|
901
|
+
*/
|
|
902
|
+
if (isNavigated) {
|
|
903
|
+
event.preventDefault();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* '/' + 'command' keydown inside a Block
|
|
909
|
+
*/
|
|
910
|
+
private commandSlashPressed(): void {
|
|
911
|
+
if (this.Blok.BlockSelection.selectedBlocks.length > 1) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
this.activateBlockSettings();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* '/' keydown inside a Block
|
|
920
|
+
* @param event - keydown
|
|
921
|
+
*/
|
|
922
|
+
private slashPressed(event: KeyboardEvent): void {
|
|
923
|
+
const wasEventTriggeredInsideBlok = this.Blok.UI.nodes.wrapper.contains(event.target as Node);
|
|
924
|
+
|
|
925
|
+
if (!wasEventTriggeredInsideBlok) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const currentBlock = this.Blok.BlockManager.currentBlock;
|
|
930
|
+
const canOpenToolbox = currentBlock?.isEmpty;
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* @todo Handle case when slash pressed when several blocks are selected
|
|
934
|
+
*/
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Toolbox will be opened only if Block is empty
|
|
938
|
+
*/
|
|
939
|
+
if (!canOpenToolbox) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* The Toolbox will be opened with immediate focus on the Search input,
|
|
945
|
+
* and '/' will be added in the search input by default — we need to prevent it and add '/' manually
|
|
946
|
+
*/
|
|
947
|
+
event.preventDefault();
|
|
948
|
+
this.Blok.Caret.insertContentAtCaretPosition('/');
|
|
949
|
+
|
|
950
|
+
this.activateToolbox();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* ENTER pressed on block
|
|
955
|
+
* @param {KeyboardEvent} event - keydown
|
|
956
|
+
*/
|
|
957
|
+
private enter(event: KeyboardEvent): void {
|
|
958
|
+
const { BlockManager, UI } = this.Blok;
|
|
959
|
+
const currentBlock = BlockManager.currentBlock;
|
|
960
|
+
|
|
961
|
+
if (currentBlock === undefined) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
|
|
967
|
+
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
|
|
968
|
+
*/
|
|
969
|
+
if (currentBlock.tool.isLineBreaksEnabled) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Opened Toolbars uses Flipper with own Enter handling
|
|
975
|
+
* Allow split block when no one button in Flipper is focused
|
|
976
|
+
*/
|
|
977
|
+
if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Allow to create line breaks by Shift+Enter
|
|
983
|
+
*
|
|
984
|
+
* Note. On iOS devices, Safari automatically treats enter after a period+space (". |") as Shift+Enter
|
|
985
|
+
* (it used for capitalizing of the first letter of the next sentence)
|
|
986
|
+
* We don't need to lead soft line break in this case — new block should be created
|
|
987
|
+
*/
|
|
988
|
+
if (event.shiftKey && !isIosDevice) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* If enter has been pressed at the start of the text, just insert paragraph Block above
|
|
994
|
+
*/
|
|
995
|
+
const blockToFocus = (() => {
|
|
996
|
+
if (currentBlock.currentInput !== undefined && isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia) {
|
|
997
|
+
this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex);
|
|
998
|
+
|
|
999
|
+
return currentBlock;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* If caret is at very end of the block, just append the new block without splitting
|
|
1004
|
+
* to prevent unnecessary dom mutation observing
|
|
1005
|
+
*/
|
|
1006
|
+
if (currentBlock.currentInput && isCaretAtEndOfInput(currentBlock.currentInput)) {
|
|
1007
|
+
return this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Split the Current Block into two blocks
|
|
1012
|
+
* Renew local current node after split
|
|
1013
|
+
*/
|
|
1014
|
+
return this.Blok.BlockManager.split();
|
|
1015
|
+
})();
|
|
1016
|
+
|
|
1017
|
+
this.Blok.Caret.setToBlock(blockToFocus);
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Show Toolbar
|
|
1021
|
+
*/
|
|
1022
|
+
this.Blok.Toolbar.moveAndOpen(blockToFocus);
|
|
1023
|
+
|
|
1024
|
+
event.preventDefault();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Handle backspace keydown on Block
|
|
1029
|
+
* @param {KeyboardEvent} event - keydown
|
|
1030
|
+
*/
|
|
1031
|
+
private backspace(event: KeyboardEvent): void {
|
|
1032
|
+
const { BlockManager, Caret } = this.Blok;
|
|
1033
|
+
const { currentBlock, previousBlock } = BlockManager;
|
|
1034
|
+
|
|
1035
|
+
if (currentBlock === undefined) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* If some fragment is selected, leave native behaviour
|
|
1041
|
+
*/
|
|
1042
|
+
if (!SelectionUtils.isCollapsed) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* If caret is not at the start, leave native behaviour
|
|
1048
|
+
*/
|
|
1049
|
+
if (!currentBlock.currentInput || !isCaretAtStartOfInput(currentBlock.currentInput)) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* All the cases below have custom behaviour, so we don't need a native one
|
|
1055
|
+
*/
|
|
1056
|
+
event.preventDefault();
|
|
1057
|
+
this.Blok.Toolbar.close();
|
|
1058
|
+
|
|
1059
|
+
const isFirstInputFocused = currentBlock.currentInput === currentBlock.firstInput;
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* For example, caret at the start of the Quote second input (caption) — just navigate previous input
|
|
1063
|
+
*/
|
|
1064
|
+
if (!isFirstInputFocused) {
|
|
1065
|
+
Caret.navigatePrevious();
|
|
1066
|
+
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Backspace at the start of the first Block should do nothing
|
|
1072
|
+
*/
|
|
1073
|
+
if (previousBlock === null) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* If prev Block is empty, it should be removed just like a character
|
|
1079
|
+
*/
|
|
1080
|
+
if (previousBlock.isEmpty) {
|
|
1081
|
+
void BlockManager.removeBlock(previousBlock);
|
|
1082
|
+
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* If current Block is empty, just remove it and set cursor to the previous Block (like we're removing line break char)
|
|
1088
|
+
*/
|
|
1089
|
+
if (currentBlock.isEmpty) {
|
|
1090
|
+
void BlockManager.removeBlock(currentBlock);
|
|
1091
|
+
|
|
1092
|
+
const newCurrentBlock = BlockManager.currentBlock;
|
|
1093
|
+
|
|
1094
|
+
newCurrentBlock && Caret.setToBlock(newCurrentBlock, Caret.positions.END);
|
|
1095
|
+
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const bothBlocksMergeable = areBlocksMergeable(previousBlock, currentBlock);
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* If Blocks could be merged, do it
|
|
1103
|
+
* Otherwise, just navigate previous block
|
|
1104
|
+
*/
|
|
1105
|
+
if (bothBlocksMergeable) {
|
|
1106
|
+
this.mergeBlocks(previousBlock, currentBlock);
|
|
1107
|
+
} else {
|
|
1108
|
+
Caret.setToBlock(previousBlock, Caret.positions.END);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Handles delete keydown on Block
|
|
1114
|
+
* Removes char after the caret.
|
|
1115
|
+
* If caret is at the end of the block, merge next block with current
|
|
1116
|
+
* @param {KeyboardEvent} event - keydown
|
|
1117
|
+
*/
|
|
1118
|
+
private delete(event: KeyboardEvent): void {
|
|
1119
|
+
const { BlockManager, Caret } = this.Blok;
|
|
1120
|
+
const { currentBlock, nextBlock } = BlockManager;
|
|
1121
|
+
|
|
1122
|
+
if (currentBlock === undefined) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* If some fragment is selected, leave native behaviour
|
|
1128
|
+
*/
|
|
1129
|
+
if (!SelectionUtils.isCollapsed) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* If caret is not at the end, leave native behaviour
|
|
1135
|
+
*/
|
|
1136
|
+
if (!currentBlock.currentInput || !isCaretAtEndOfInput(currentBlock.currentInput)) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* All the cases below have custom behaviour, so we don't need a native one
|
|
1142
|
+
*/
|
|
1143
|
+
event.preventDefault();
|
|
1144
|
+
this.Blok.Toolbar.close();
|
|
1145
|
+
|
|
1146
|
+
const isLastInputFocused = currentBlock.currentInput === currentBlock.lastInput;
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* For example, caret at the end of the Quote first input (quote text) — just navigate next input (caption)
|
|
1150
|
+
*/
|
|
1151
|
+
if (!isLastInputFocused) {
|
|
1152
|
+
Caret.navigateNext();
|
|
1153
|
+
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Delete at the end of the last Block should do nothing
|
|
1159
|
+
*/
|
|
1160
|
+
if (nextBlock === null) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* If next Block is empty, it should be removed just like a character
|
|
1166
|
+
*/
|
|
1167
|
+
if (nextBlock.isEmpty) {
|
|
1168
|
+
void BlockManager.removeBlock(nextBlock);
|
|
1169
|
+
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* If current Block is empty, just remove it and set cursor to the next Block (like we're removing line break char)
|
|
1175
|
+
*/
|
|
1176
|
+
if (currentBlock.isEmpty) {
|
|
1177
|
+
void BlockManager.removeBlock(currentBlock);
|
|
1178
|
+
|
|
1179
|
+
Caret.setToBlock(nextBlock, Caret.positions.START);
|
|
1180
|
+
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const bothBlocksMergeable = areBlocksMergeable(currentBlock, nextBlock);
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* If Blocks could be merged, do it
|
|
1188
|
+
* Otherwise, just navigate to the next block
|
|
1189
|
+
*/
|
|
1190
|
+
if (bothBlocksMergeable) {
|
|
1191
|
+
this.mergeBlocks(currentBlock, nextBlock);
|
|
1192
|
+
} else {
|
|
1193
|
+
Caret.setToBlock(nextBlock, Caret.positions.START);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Merge passed Blocks
|
|
1199
|
+
* @param targetBlock - to which Block we want to merge
|
|
1200
|
+
* @param blockToMerge - what Block we want to merge
|
|
1201
|
+
*/
|
|
1202
|
+
private mergeBlocks(targetBlock: Block, blockToMerge: Block): void {
|
|
1203
|
+
const { BlockManager, Toolbar } = this.Blok;
|
|
1204
|
+
|
|
1205
|
+
if (targetBlock.lastInput === undefined) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
focus(targetBlock.lastInput, false);
|
|
1210
|
+
|
|
1211
|
+
BlockManager
|
|
1212
|
+
.mergeBlocks(targetBlock, blockToMerge)
|
|
1213
|
+
.then(() => {
|
|
1214
|
+
Toolbar.close();
|
|
1215
|
+
})
|
|
1216
|
+
.catch(() => {
|
|
1217
|
+
// Error handling for mergeBlocks
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Handle right and down keyboard keys
|
|
1223
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
1224
|
+
*/
|
|
1225
|
+
private arrowRightAndDown(event: KeyboardEvent): void {
|
|
1226
|
+
const keyCode = this.getKeyCode(event);
|
|
1227
|
+
|
|
1228
|
+
if (keyCode === null) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Skip handling if this is a block movement shortcut (Cmd/Ctrl+Shift+Down)
|
|
1234
|
+
* Let the shortcut system handle it instead
|
|
1235
|
+
*/
|
|
1236
|
+
if (isBlockMovementShortcut(event, 'down')) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const isFlipperCombination = Flipper.usedKeys.includes(keyCode) &&
|
|
1241
|
+
(!event.shiftKey || keyCode === keyCodes.TAB);
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Arrows might be handled on toolbars by flipper
|
|
1245
|
+
* Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT
|
|
1246
|
+
*/
|
|
1247
|
+
if (this.Blok.UI.someToolbarOpened && isFlipperCombination) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Close Toolbar when user moves cursor, but keep toolbars open if the user
|
|
1253
|
+
* is extending selection with the Shift key so inline interactions remain available.
|
|
1254
|
+
*/
|
|
1255
|
+
if (!event.shiftKey) {
|
|
1256
|
+
this.Blok.Toolbar.close();
|
|
1257
|
+
this.Blok.InlineToolbar.close();
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const selection = SelectionUtils.get();
|
|
1261
|
+
|
|
1262
|
+
if (selection?.anchorNode && !this.Blok.BlockSelection.anyBlockSelected) {
|
|
1263
|
+
this.Blok.BlockManager.setCurrentBlockByChildNode(selection.anchorNode);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const { currentBlock } = this.Blok.BlockManager;
|
|
1267
|
+
const eventTarget = event.target as HTMLElement | null;
|
|
1268
|
+
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1269
|
+
const fallbackInputCandidates: Array<HTMLElement | undefined | null> = [
|
|
1270
|
+
currentBlock?.inputs.find((input) => eventTarget !== null && input.contains(eventTarget)),
|
|
1271
|
+
currentBlock?.inputs.find((input) => activeElement !== null && input.contains(activeElement)),
|
|
1272
|
+
eventTarget?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
|
|
1273
|
+
activeElement?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
|
|
1274
|
+
];
|
|
1275
|
+
const caretInput = currentBlock?.currentInput ?? fallbackInputCandidates.find((candidate): candidate is HTMLElement => {
|
|
1276
|
+
return candidate instanceof HTMLElement;
|
|
1277
|
+
});
|
|
1278
|
+
const caretAtEnd = caretInput !== undefined ? isCaretAtEndOfInput(caretInput) : undefined;
|
|
1279
|
+
const shouldEnableCBS = caretAtEnd || this.Blok.BlockSelection.anyBlockSelected;
|
|
1280
|
+
|
|
1281
|
+
const isShiftDownKey = event.shiftKey && keyCode === keyCodes.DOWN;
|
|
1282
|
+
|
|
1283
|
+
if (isShiftDownKey && shouldEnableCBS) {
|
|
1284
|
+
this.Blok.CrossBlockSelection.toggleBlockSelectedState();
|
|
1285
|
+
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (isShiftDownKey) {
|
|
1290
|
+
void this.Blok.InlineToolbar.tryToShow();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const isPlainRightKey = keyCode === keyCodes.RIGHT && !event.shiftKey && !this.isRtl;
|
|
1294
|
+
|
|
1295
|
+
const nbpsTarget = isPlainRightKey && caretInput instanceof HTMLElement
|
|
1296
|
+
? findNbspAfterEmptyInline(caretInput)
|
|
1297
|
+
: null;
|
|
1298
|
+
|
|
1299
|
+
if (nbpsTarget !== null) {
|
|
1300
|
+
SelectionUtils.setCursor(nbpsTarget.node as unknown as HTMLElement, nbpsTarget.offset);
|
|
1301
|
+
event.preventDefault();
|
|
1302
|
+
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Determine navigation type based on key pressed:
|
|
1308
|
+
* - Arrow Down: use vertical navigation (Notion-style line-by-line)
|
|
1309
|
+
* - Arrow Right: use horizontal navigation (character-by-character)
|
|
1310
|
+
*/
|
|
1311
|
+
const isDownKey = keyCode === keyCodes.DOWN;
|
|
1312
|
+
const isRightKey = keyCode === keyCodes.RIGHT && !this.isRtl;
|
|
1313
|
+
|
|
1314
|
+
const isNavigated = (() => {
|
|
1315
|
+
if (isDownKey) {
|
|
1316
|
+
/**
|
|
1317
|
+
* Arrow Down: Notion-style vertical navigation
|
|
1318
|
+
* Only navigate to next block when caret is at the last line
|
|
1319
|
+
*/
|
|
1320
|
+
return this.Blok.Caret.navigateVerticalNext();
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (isRightKey) {
|
|
1324
|
+
/**
|
|
1325
|
+
* Arrow Right: horizontal navigation
|
|
1326
|
+
* Navigate to next block when caret is at the end of input
|
|
1327
|
+
*/
|
|
1328
|
+
return this.Blok.Caret.navigateNext();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return false;
|
|
1332
|
+
})();
|
|
1333
|
+
|
|
1334
|
+
if (isNavigated) {
|
|
1335
|
+
/**
|
|
1336
|
+
* Default behaviour moves cursor by 1 character, we need to prevent it
|
|
1337
|
+
*/
|
|
1338
|
+
event.preventDefault();
|
|
1339
|
+
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* After caret is set, update Block input index
|
|
1345
|
+
*/
|
|
1346
|
+
delay(() => {
|
|
1347
|
+
/** Check currentBlock for case when user moves selection out of Blok */
|
|
1348
|
+
if (this.Blok.BlockManager.currentBlock) {
|
|
1349
|
+
this.Blok.BlockManager.currentBlock.updateCurrentInput();
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
}, 20)();
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Clear blocks selection by arrows
|
|
1356
|
+
*/
|
|
1357
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Handle left and up keyboard keys
|
|
1362
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
1363
|
+
*/
|
|
1364
|
+
private arrowLeftAndUp(event: KeyboardEvent): void {
|
|
1365
|
+
/**
|
|
1366
|
+
* Arrows might be handled on toolbars by flipper
|
|
1367
|
+
* Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT
|
|
1368
|
+
*/
|
|
1369
|
+
const toolbarOpened = this.Blok.UI.someToolbarOpened;
|
|
1370
|
+
|
|
1371
|
+
const keyCode = this.getKeyCode(event);
|
|
1372
|
+
|
|
1373
|
+
if (keyCode === null) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Skip handling if this is a block movement shortcut (Cmd/Ctrl+Shift+Up)
|
|
1379
|
+
* Let the shortcut system handle it instead
|
|
1380
|
+
*/
|
|
1381
|
+
if (isBlockMovementShortcut(event, 'up')) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (toolbarOpened && Flipper.usedKeys.includes(keyCode) && (!event.shiftKey || keyCode === keyCodes.TAB)) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (toolbarOpened) {
|
|
1390
|
+
this.Blok.UI.closeAllToolbars();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Close Toolbar when user moves cursor, but preserve it for Shift-based selection changes.
|
|
1395
|
+
*/
|
|
1396
|
+
if (!event.shiftKey) {
|
|
1397
|
+
this.Blok.Toolbar.close();
|
|
1398
|
+
this.Blok.InlineToolbar.close();
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const selection = window.getSelection();
|
|
1402
|
+
|
|
1403
|
+
if (selection?.anchorNode && !this.Blok.BlockSelection.anyBlockSelected) {
|
|
1404
|
+
this.Blok.BlockManager.setCurrentBlockByChildNode(selection.anchorNode);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const { currentBlock } = this.Blok.BlockManager;
|
|
1408
|
+
const eventTarget = event.target as HTMLElement | null;
|
|
1409
|
+
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1410
|
+
const fallbackInputCandidates: Array<HTMLElement | undefined | null> = [
|
|
1411
|
+
currentBlock?.inputs.find((input) => eventTarget !== null && input.contains(eventTarget)),
|
|
1412
|
+
currentBlock?.inputs.find((input) => activeElement !== null && input.contains(activeElement)),
|
|
1413
|
+
eventTarget?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
|
|
1414
|
+
activeElement?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
|
|
1415
|
+
];
|
|
1416
|
+
const caretInput = currentBlock?.currentInput ?? fallbackInputCandidates.find((candidate): candidate is HTMLElement => {
|
|
1417
|
+
return candidate instanceof HTMLElement;
|
|
1418
|
+
});
|
|
1419
|
+
const caretAtStart = caretInput !== undefined ? isCaretAtStartOfInput(caretInput) : undefined;
|
|
1420
|
+
const shouldEnableCBS = caretAtStart || this.Blok.BlockSelection.anyBlockSelected;
|
|
1421
|
+
|
|
1422
|
+
const isShiftUpKey = event.shiftKey && keyCode === keyCodes.UP;
|
|
1423
|
+
|
|
1424
|
+
if (isShiftUpKey && shouldEnableCBS) {
|
|
1425
|
+
this.Blok.CrossBlockSelection.toggleBlockSelectedState(false);
|
|
1426
|
+
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (isShiftUpKey) {
|
|
1431
|
+
void this.Blok.InlineToolbar.tryToShow();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Determine navigation type based on key pressed:
|
|
1436
|
+
* - Arrow Up: use vertical navigation (Notion-style line-by-line)
|
|
1437
|
+
* - Arrow Left: use horizontal navigation (character-by-character)
|
|
1438
|
+
*/
|
|
1439
|
+
const isUpKey = keyCode === keyCodes.UP;
|
|
1440
|
+
const isLeftKey = keyCode === keyCodes.LEFT && !this.isRtl;
|
|
1441
|
+
|
|
1442
|
+
const isNavigated = (() => {
|
|
1443
|
+
if (isUpKey) {
|
|
1444
|
+
/**
|
|
1445
|
+
* Arrow Up: Notion-style vertical navigation
|
|
1446
|
+
* Only navigate to previous block when caret is at the first line
|
|
1447
|
+
*/
|
|
1448
|
+
return this.Blok.Caret.navigateVerticalPrevious();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (isLeftKey) {
|
|
1452
|
+
/**
|
|
1453
|
+
* Arrow Left: horizontal navigation
|
|
1454
|
+
* Navigate to previous block when caret is at the start of input
|
|
1455
|
+
*/
|
|
1456
|
+
return this.Blok.Caret.navigatePrevious();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return false;
|
|
1460
|
+
})();
|
|
1461
|
+
|
|
1462
|
+
if (isNavigated) {
|
|
1463
|
+
/**
|
|
1464
|
+
* Default behaviour moves cursor by 1 character, we need to prevent it
|
|
1465
|
+
*/
|
|
1466
|
+
event.preventDefault();
|
|
1467
|
+
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* After caret is set, update Block input index
|
|
1473
|
+
*/
|
|
1474
|
+
delay(() => {
|
|
1475
|
+
/** Check currentBlock for case when user ends selection out of Blok and then press arrow-key */
|
|
1476
|
+
if (this.Blok.BlockManager.currentBlock) {
|
|
1477
|
+
this.Blok.BlockManager.currentBlock.updateCurrentInput();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
}, 20)();
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Clear blocks selection by arrows
|
|
1484
|
+
*/
|
|
1485
|
+
this.Blok.BlockSelection.clearSelection(event);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Cases when we need to close Toolbar
|
|
1490
|
+
* @param {KeyboardEvent} event - keyboard event
|
|
1491
|
+
*/
|
|
1492
|
+
private needToolbarClosing(event: KeyboardEvent): boolean {
|
|
1493
|
+
const keyCode = this.getKeyCode(event);
|
|
1494
|
+
const isEnter = keyCode === keyCodes.ENTER;
|
|
1495
|
+
const isTab = keyCode === keyCodes.TAB;
|
|
1496
|
+
const toolboxItemSelected = (isEnter && this.Blok.Toolbar.toolbox.opened);
|
|
1497
|
+
const blockSettingsItemSelected = (isEnter && this.Blok.BlockSettings.opened);
|
|
1498
|
+
const inlineToolbarItemSelected = (isEnter && this.Blok.InlineToolbar.opened);
|
|
1499
|
+
const flippingToolbarItems = isTab;
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* When Toolbox is open, allow typing for inline slash search filtering.
|
|
1503
|
+
* Only close on Enter (to select item) or Tab (to navigate).
|
|
1504
|
+
*/
|
|
1505
|
+
const toolboxOpenForInlineSearch = this.Blok.Toolbar.toolbox.opened && !isEnter && !isTab;
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Do not close Toolbar in cases:
|
|
1509
|
+
* 1. ShiftKey pressed (or combination with shiftKey)
|
|
1510
|
+
* 2. When Toolbar is opened and Tab leafs its Tools
|
|
1511
|
+
* 3. When Toolbar's component is opened and some its item selected
|
|
1512
|
+
* 4. When Toolbox is open for inline slash search (allow typing to filter)
|
|
1513
|
+
*/
|
|
1514
|
+
return !(event.shiftKey ||
|
|
1515
|
+
flippingToolbarItems ||
|
|
1516
|
+
toolboxItemSelected ||
|
|
1517
|
+
blockSettingsItemSelected ||
|
|
1518
|
+
inlineToolbarItemSelected ||
|
|
1519
|
+
toolboxOpenForInlineSearch
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* If Toolbox is not open, then just open it and show plus button
|
|
1525
|
+
*/
|
|
1526
|
+
private activateToolbox(): void {
|
|
1527
|
+
if (!this.Blok.Toolbar.opened) {
|
|
1528
|
+
this.Blok.Toolbar.moveAndOpen();
|
|
1529
|
+
} // else Flipper will leaf through it
|
|
1530
|
+
|
|
1531
|
+
this.Blok.Toolbar.toolbox.open();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Open Toolbar and show BlockSettings before flipping Tools
|
|
1536
|
+
*/
|
|
1537
|
+
private activateBlockSettings(): void {
|
|
1538
|
+
if (!this.Blok.Toolbar.opened) {
|
|
1539
|
+
this.Blok.Toolbar.moveAndOpen();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* If BlockSettings is not open, then open BlockSettings
|
|
1544
|
+
* Next Tab press will leaf Settings Buttons
|
|
1545
|
+
*/
|
|
1546
|
+
if (!this.Blok.BlockSettings.opened) {
|
|
1547
|
+
/**
|
|
1548
|
+
* @todo Debug the case when we set caret to some block, hovering another block
|
|
1549
|
+
* — wrong settings will be opened.
|
|
1550
|
+
* To fix it, we should refactor the Block Settings module — make it a standalone class, like the Toolbox
|
|
1551
|
+
*/
|
|
1552
|
+
void Promise
|
|
1553
|
+
.resolve(this.Blok.BlockSettings.open())
|
|
1554
|
+
.catch(() => {
|
|
1555
|
+
// Error handling for BlockSettings.open
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Convert KeyboardEvent.key or code to the legacy numeric keyCode
|
|
1562
|
+
* @param event - keyboard event
|
|
1563
|
+
*/
|
|
1564
|
+
private getKeyCode(event: KeyboardEvent): number | null {
|
|
1565
|
+
const keyFromEvent = event.key && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.key];
|
|
1566
|
+
|
|
1567
|
+
if (keyFromEvent !== undefined && typeof keyFromEvent === 'number') {
|
|
1568
|
+
return keyFromEvent;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const codeFromEvent = event.code && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.code];
|
|
1572
|
+
|
|
1573
|
+
if (codeFromEvent !== undefined && typeof codeFromEvent === 'number') {
|
|
1574
|
+
return codeFromEvent;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Detect whether KeyDown should be treated as printable input
|
|
1582
|
+
* @param event - keyboard event
|
|
1583
|
+
*/
|
|
1584
|
+
private isPrintableKeyEvent(event: KeyboardEvent): boolean {
|
|
1585
|
+
if (!event.key) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return event.key.length === 1 || PRINTABLE_SPECIAL_KEYS.has(event.key);
|
|
1590
|
+
}
|
|
1591
|
+
}
|