@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,1356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class BlockManager
|
|
3
|
+
* @classdesc Manage blok`s blocks storage and appearance
|
|
4
|
+
* @module BlockManager
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
import { Block, BlockToolAPI } from '../block';
|
|
8
|
+
import { Module } from '../__module';
|
|
9
|
+
import { Dom as $ } from '../dom';
|
|
10
|
+
import { isEmpty, isObject, isString, log } from '../utils';
|
|
11
|
+
import { Blocks } from '../blocks';
|
|
12
|
+
import type { BlockToolData, PasteEvent, SanitizerConfig } from '../../../types';
|
|
13
|
+
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
|
14
|
+
import { BlockAPI } from '../block/api';
|
|
15
|
+
import type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';
|
|
16
|
+
import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';
|
|
17
|
+
import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
|
|
18
|
+
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
|
19
|
+
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
|
20
|
+
import { BlockChanged } from '../events';
|
|
21
|
+
import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
|
|
22
|
+
import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';
|
|
23
|
+
import { PromiseQueue } from '../utils/promise-queue';
|
|
24
|
+
import { DATA_ATTR, createSelector } from '../constants';
|
|
25
|
+
import { Shortcuts } from '../utils/shortcuts';
|
|
26
|
+
import { announce } from '../utils/announcer';
|
|
27
|
+
|
|
28
|
+
type BlocksStore = Blocks & {
|
|
29
|
+
[index: number]: Block | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {BlockManager} BlockManager
|
|
34
|
+
* @property {number} currentBlockIndex - Index of current working block
|
|
35
|
+
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
|
|
36
|
+
*/
|
|
37
|
+
export class BlockManager extends Module {
|
|
38
|
+
/**
|
|
39
|
+
* Returns current Block index
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
public get currentBlockIndex(): number {
|
|
43
|
+
return this._currentBlockIndex;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set current Block index and fire Block lifecycle callbacks
|
|
48
|
+
* @param {number} newIndex - index of Block to set as current
|
|
49
|
+
*/
|
|
50
|
+
public set currentBlockIndex(newIndex: number) {
|
|
51
|
+
this._currentBlockIndex = newIndex;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* returns first Block
|
|
56
|
+
* @returns {Block}
|
|
57
|
+
*/
|
|
58
|
+
public get firstBlock(): Block | undefined {
|
|
59
|
+
return this.blocksStore[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* returns last Block
|
|
64
|
+
* @returns {Block}
|
|
65
|
+
*/
|
|
66
|
+
public get lastBlock(): Block | undefined {
|
|
67
|
+
return this.blocksStore[this.blocksStore.length - 1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get current Block instance
|
|
72
|
+
* @returns {Block}
|
|
73
|
+
*/
|
|
74
|
+
public get currentBlock(): Block | undefined {
|
|
75
|
+
return this.blocksStore[this.currentBlockIndex];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set passed Block as a current
|
|
80
|
+
* @param block - block to set as a current
|
|
81
|
+
*/
|
|
82
|
+
public set currentBlock(block: Block | undefined) {
|
|
83
|
+
if (block === undefined) {
|
|
84
|
+
this.unsetCurrentBlock();
|
|
85
|
+
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.currentBlockIndex = this.getBlockIndex(block);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns next Block instance
|
|
94
|
+
* @returns {Block|null}
|
|
95
|
+
*/
|
|
96
|
+
public get nextBlock(): Block | null {
|
|
97
|
+
const isLastBlock = this.currentBlockIndex === (this.blocksStore.length - 1);
|
|
98
|
+
|
|
99
|
+
if (isLastBlock) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nextBlock = this.blocksStore[this.currentBlockIndex + 1];
|
|
104
|
+
|
|
105
|
+
return nextBlock ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return first Block with inputs after current Block
|
|
110
|
+
* @returns {Block | undefined}
|
|
111
|
+
*/
|
|
112
|
+
public get nextContentfulBlock(): Block | undefined {
|
|
113
|
+
const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);
|
|
114
|
+
|
|
115
|
+
return nextBlocks.find((block) => !!block.inputs.length);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Return first Block with inputs before current Block
|
|
120
|
+
* @returns {Block | undefined}
|
|
121
|
+
*/
|
|
122
|
+
public get previousContentfulBlock(): Block | undefined {
|
|
123
|
+
const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();
|
|
124
|
+
|
|
125
|
+
return previousBlocks.find((block) => !!block.inputs.length);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns previous Block instance
|
|
130
|
+
* @returns {Block|null}
|
|
131
|
+
*/
|
|
132
|
+
public get previousBlock(): Block | null {
|
|
133
|
+
const isFirstBlock = this.currentBlockIndex === 0;
|
|
134
|
+
|
|
135
|
+
if (isFirstBlock) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const previousBlock = this.blocksStore[this.currentBlockIndex - 1];
|
|
140
|
+
|
|
141
|
+
return previousBlock ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get array of Block instances
|
|
146
|
+
* @returns {Block[]} {@link Blocks#array}
|
|
147
|
+
*/
|
|
148
|
+
public get blocks(): Block[] {
|
|
149
|
+
return this.blocksStore.array;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if each Block is empty
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
public get isBlokEmpty(): boolean {
|
|
157
|
+
return this.blocks.every((block) => block.isEmpty);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Index of current working block
|
|
162
|
+
* @type {number}
|
|
163
|
+
*/
|
|
164
|
+
private _currentBlockIndex = -1;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Proxy for Blocks instance {@link Blocks}
|
|
168
|
+
* @type {Proxy}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
private _blocks: BlocksStore | null = null;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Registered keyboard shortcut names for cleanup
|
|
175
|
+
*/
|
|
176
|
+
private registeredShortcuts: string[] = [];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Should be called after Blok.UI preparation
|
|
180
|
+
* Define this._blocks property
|
|
181
|
+
*/
|
|
182
|
+
public prepare(): void {
|
|
183
|
+
const blocks = new Blocks(this.Blok.UI.nodes.redactor);
|
|
184
|
+
/**
|
|
185
|
+
* We need to use Proxy to overload set/get [] operator.
|
|
186
|
+
* So we can use array-like syntax to access blocks
|
|
187
|
+
* @example
|
|
188
|
+
* this._blocks[0] = new Block(...);
|
|
189
|
+
*
|
|
190
|
+
* block = this._blocks[0];
|
|
191
|
+
* @todo proxy the enumerate method
|
|
192
|
+
* @type {Proxy}
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
this._blocks = new Proxy(blocks, {
|
|
196
|
+
set: Blocks.set,
|
|
197
|
+
get: Blocks.get,
|
|
198
|
+
}) as BlocksStore;
|
|
199
|
+
|
|
200
|
+
/** Copy event */
|
|
201
|
+
this.listeners.on(
|
|
202
|
+
document,
|
|
203
|
+
'copy',
|
|
204
|
+
(event: Event) => {
|
|
205
|
+
this.Blok.BlockEvents.handleCommandC(event as ClipboardEvent);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
this.setupKeyboardShortcuts();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Toggle read-only state
|
|
214
|
+
*
|
|
215
|
+
* If readOnly is true:
|
|
216
|
+
* - Unbind event handlers from created Blocks
|
|
217
|
+
*
|
|
218
|
+
* if readOnly is false:
|
|
219
|
+
* - Bind event handlers to all existing Blocks
|
|
220
|
+
* @param {boolean} readOnlyEnabled - "read only" state
|
|
221
|
+
*/
|
|
222
|
+
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
|
223
|
+
if (!readOnlyEnabled) {
|
|
224
|
+
this.enableModuleBindings();
|
|
225
|
+
} else {
|
|
226
|
+
this.disableModuleBindings();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates Block instance by tool name
|
|
232
|
+
* @param {object} options - block creation options
|
|
233
|
+
* @param {string} options.tool - tools passed in blok config {@link BlokConfig#tools}
|
|
234
|
+
* @param {string} [options.id] - unique id for this block
|
|
235
|
+
* @param {BlockToolData} [options.data] - constructor params
|
|
236
|
+
* @param {string} [options.parentId] - parent block id for hierarchical structure
|
|
237
|
+
* @param {string[]} [options.contentIds] - array of child block ids
|
|
238
|
+
* @returns {Block}
|
|
239
|
+
*/
|
|
240
|
+
public composeBlock({
|
|
241
|
+
tool: name,
|
|
242
|
+
data = {},
|
|
243
|
+
id = undefined,
|
|
244
|
+
tunes: tunesData = {},
|
|
245
|
+
parentId,
|
|
246
|
+
contentIds,
|
|
247
|
+
}: {
|
|
248
|
+
tool: string;
|
|
249
|
+
id?: string;
|
|
250
|
+
data?: BlockToolData;
|
|
251
|
+
tunes?: {[name: string]: BlockTuneData};
|
|
252
|
+
parentId?: string;
|
|
253
|
+
contentIds?: string[];
|
|
254
|
+
}): Block {
|
|
255
|
+
const readOnly = this.Blok.ReadOnly.isEnabled;
|
|
256
|
+
const tool = this.Blok.Tools.blockTools.get(name);
|
|
257
|
+
|
|
258
|
+
if (tool === undefined) {
|
|
259
|
+
throw new Error(`Could not compose Block. Tool «${name}» not found.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const block = new Block({
|
|
263
|
+
id,
|
|
264
|
+
data,
|
|
265
|
+
tool,
|
|
266
|
+
api: this.Blok.API,
|
|
267
|
+
readOnly,
|
|
268
|
+
tunesData,
|
|
269
|
+
parentId,
|
|
270
|
+
contentIds,
|
|
271
|
+
}, this.eventsDispatcher);
|
|
272
|
+
|
|
273
|
+
if (!readOnly) {
|
|
274
|
+
window.requestIdleCallback(() => {
|
|
275
|
+
this.bindBlockEvents(block);
|
|
276
|
+
}, { timeout: 2000 });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return block;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Insert new block into _blocks
|
|
284
|
+
* @param {object} options - insert options
|
|
285
|
+
* @param {string} [options.id] - block's unique id
|
|
286
|
+
* @param {string} [options.tool] - plugin name, by default method inserts the default block type
|
|
287
|
+
* @param {object} [options.data] - plugin data
|
|
288
|
+
* @param {number} [options.index] - index where to insert new Block
|
|
289
|
+
* @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index
|
|
290
|
+
* @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one
|
|
291
|
+
* @returns {Block}
|
|
292
|
+
*/
|
|
293
|
+
public insert({
|
|
294
|
+
id = undefined,
|
|
295
|
+
tool,
|
|
296
|
+
data,
|
|
297
|
+
index,
|
|
298
|
+
needToFocus = true,
|
|
299
|
+
replace = false,
|
|
300
|
+
tunes,
|
|
301
|
+
}: {
|
|
302
|
+
id?: string;
|
|
303
|
+
tool?: string;
|
|
304
|
+
data?: BlockToolData;
|
|
305
|
+
index?: number;
|
|
306
|
+
needToFocus?: boolean;
|
|
307
|
+
replace?: boolean;
|
|
308
|
+
tunes?: {[name: string]: BlockTuneData};
|
|
309
|
+
} = {}): Block {
|
|
310
|
+
const targetIndex = index ?? this.currentBlockIndex + (replace ? 0 : 1);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* If we're replacing a block, stop watching for mutations immediately to prevent
|
|
314
|
+
* spurious block-changed events from DOM manipulations (like focus restoration)
|
|
315
|
+
* that may occur before the block is fully replaced.
|
|
316
|
+
*/
|
|
317
|
+
if (replace) {
|
|
318
|
+
this.getBlockByIndex(targetIndex)?.unwatchBlockMutations();
|
|
319
|
+
}
|
|
320
|
+
const toolName = tool ?? this.config.defaultBlock;
|
|
321
|
+
|
|
322
|
+
if (toolName === undefined) {
|
|
323
|
+
throw new Error('Could not insert Block. Tool name is not specified.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const composeOptions: {
|
|
327
|
+
tool: string;
|
|
328
|
+
id?: string;
|
|
329
|
+
data?: BlockToolData;
|
|
330
|
+
tunes?: {[name: string]: BlockTuneData};
|
|
331
|
+
} = {
|
|
332
|
+
tool: toolName,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
if (id !== undefined) {
|
|
336
|
+
composeOptions.id = id;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (data !== undefined) {
|
|
340
|
+
composeOptions.data = data;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (tunes !== undefined) {
|
|
344
|
+
composeOptions.tunes = tunes;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const block = this.composeBlock(composeOptions);
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* In case of block replacing (Converting OR from Toolbox or Shortcut on empty block OR on-paste to empty block)
|
|
351
|
+
* we need to dispatch the 'block-removing' event for the replacing block
|
|
352
|
+
*/
|
|
353
|
+
const blockToReplace = replace ? this.getBlockByIndex(targetIndex) : undefined;
|
|
354
|
+
|
|
355
|
+
if (replace && blockToReplace === undefined) {
|
|
356
|
+
throw new Error(`Could not replace Block at index ${targetIndex}. Block not found.`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (replace && blockToReplace !== undefined) {
|
|
360
|
+
this.blockDidMutated(BlockRemovedMutationType, blockToReplace, {
|
|
361
|
+
index: targetIndex,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.blocksStore.insert(targetIndex, block, replace);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Force call of didMutated event on Block insertion
|
|
369
|
+
*/
|
|
370
|
+
this.blockDidMutated(BlockAddedMutationType, block, {
|
|
371
|
+
index: targetIndex,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (needToFocus) {
|
|
375
|
+
this.currentBlockIndex = targetIndex;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!needToFocus && targetIndex <= this.currentBlockIndex) {
|
|
379
|
+
this.currentBlockIndex++;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return block;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Inserts several blocks at once
|
|
387
|
+
* @param blocks - blocks to insert
|
|
388
|
+
* @param index - index where to insert
|
|
389
|
+
*/
|
|
390
|
+
public insertMany(blocks: Block[], index = 0): void {
|
|
391
|
+
this.blocksStore.insertMany(blocks, index);
|
|
392
|
+
|
|
393
|
+
// Apply indentation for blocks with parentId (hierarchical structure)
|
|
394
|
+
blocks.forEach(block => {
|
|
395
|
+
if (block.parentId !== null) {
|
|
396
|
+
this.updateBlockIndentation(block);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Update Block data.
|
|
403
|
+
*
|
|
404
|
+
* Currently we don't have an 'update' method in the Tools API, so we just create a new block with the same id and type
|
|
405
|
+
* Should not trigger 'block-removed' or 'block-added' events.
|
|
406
|
+
*
|
|
407
|
+
* If neither data nor tunes is provided, return the provided block instead.
|
|
408
|
+
* @param block - block to update
|
|
409
|
+
* @param data - (optional) new data
|
|
410
|
+
* @param tunes - (optional) tune data
|
|
411
|
+
*/
|
|
412
|
+
public async update(block: Block, data?: Partial<BlockToolData>, tunes?: {[name: string]: BlockTuneData}): Promise<Block> {
|
|
413
|
+
if (!data && !tunes) {
|
|
414
|
+
return block;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const existingData = await block.data;
|
|
418
|
+
|
|
419
|
+
const newBlock = this.composeBlock({
|
|
420
|
+
id: block.id,
|
|
421
|
+
tool: block.name,
|
|
422
|
+
data: Object.assign({}, existingData, data ?? {}),
|
|
423
|
+
tunes: tunes ?? block.tunes,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const blockIndex = this.getBlockIndex(block);
|
|
427
|
+
|
|
428
|
+
this.blocksStore.replace(blockIndex, newBlock);
|
|
429
|
+
|
|
430
|
+
this.blockDidMutated(BlockChangedMutationType, newBlock, {
|
|
431
|
+
index: blockIndex,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return newBlock;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Replace passed Block with the new one with specified Tool and data
|
|
439
|
+
* @param block - block to replace
|
|
440
|
+
* @param newTool - new Tool name
|
|
441
|
+
* @param data - new Tool data
|
|
442
|
+
*/
|
|
443
|
+
public replace(block: Block, newTool: string, data: BlockToolData): Block {
|
|
444
|
+
const blockIndex = this.getBlockIndex(block);
|
|
445
|
+
|
|
446
|
+
return this.insert({
|
|
447
|
+
tool: newTool,
|
|
448
|
+
data,
|
|
449
|
+
index: blockIndex,
|
|
450
|
+
replace: true,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Returns the proxied Blocks storage ensuring it is initialized.
|
|
456
|
+
* @throws {Error} if the storage is not prepared.
|
|
457
|
+
*/
|
|
458
|
+
private get blocksStore(): BlocksStore {
|
|
459
|
+
if (this._blocks === null) {
|
|
460
|
+
throw new Error('BlockManager: blocks store is not initialized. Call prepare() before accessing blocks.');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return this._blocks;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Insert pasted content. Call onPaste callback after insert.
|
|
468
|
+
* @param {string} toolName - name of Tool to insert
|
|
469
|
+
* @param {PasteEvent} pasteEvent - pasted data
|
|
470
|
+
* @param {boolean} replace - should replace current block
|
|
471
|
+
*/
|
|
472
|
+
public async paste(
|
|
473
|
+
toolName: string,
|
|
474
|
+
pasteEvent: PasteEvent,
|
|
475
|
+
replace = false
|
|
476
|
+
): Promise<Block> {
|
|
477
|
+
const block = this.insert({
|
|
478
|
+
tool: toolName,
|
|
479
|
+
replace,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
/**
|
|
484
|
+
* We need to call onPaste after Block will be ready
|
|
485
|
+
* because onPaste could change tool's root element, and we need to do that after block.watchBlockMutations() bound
|
|
486
|
+
* to detect tool root element change
|
|
487
|
+
* @todo make this.insert() awaitable and remove requestIdleCallback
|
|
488
|
+
*/
|
|
489
|
+
await block.ready;
|
|
490
|
+
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* onPaste might cause the tool to replace its root element (e.g., Header changing level).
|
|
494
|
+
* Since mutation observers are set up asynchronously via requestIdleCallback,
|
|
495
|
+
* we need to manually refresh the tool element reference here.
|
|
496
|
+
*/
|
|
497
|
+
block.refreshToolRootElement();
|
|
498
|
+
} catch (e) {
|
|
499
|
+
log(`${toolName}: onPaste callback call is failed`, 'error', e);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return block;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Insert new default block at passed index
|
|
507
|
+
* @param {number} index - index where Block should be inserted
|
|
508
|
+
* @param {boolean} needToFocus - if true, updates current Block index
|
|
509
|
+
*
|
|
510
|
+
* TODO: Remove method and use insert() with index instead (?)
|
|
511
|
+
* @returns {Block} inserted Block
|
|
512
|
+
*/
|
|
513
|
+
public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block {
|
|
514
|
+
const defaultTool = this.config.defaultBlock;
|
|
515
|
+
|
|
516
|
+
if (defaultTool === undefined) {
|
|
517
|
+
throw new Error('Could not insert default Block. Default block tool is not defined in the configuration.');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return this.insert({
|
|
521
|
+
tool: defaultTool,
|
|
522
|
+
index,
|
|
523
|
+
needToFocus,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Always inserts at the end
|
|
529
|
+
* @returns {Block}
|
|
530
|
+
*/
|
|
531
|
+
public insertAtEnd(): Block {
|
|
532
|
+
/**
|
|
533
|
+
* Define new value for current block index
|
|
534
|
+
*/
|
|
535
|
+
this.currentBlockIndex = this.blocks.length - 1;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Insert the default typed block
|
|
539
|
+
*/
|
|
540
|
+
return this.insert();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Merge two blocks
|
|
545
|
+
* @param {Block} targetBlock - previous block will be append to this block
|
|
546
|
+
* @param {Block} blockToMerge - block that will be merged with target block
|
|
547
|
+
* @returns {Promise} - the sequence that can be continued
|
|
548
|
+
*/
|
|
549
|
+
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
|
|
550
|
+
const completeMerge = async (data: BlockToolData): Promise<void> => {
|
|
551
|
+
await targetBlock.mergeWith(data);
|
|
552
|
+
await this.removeBlock(blockToMerge);
|
|
553
|
+
this.currentBlockIndex = this.blocksStore.indexOf(targetBlock);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* We can merge:
|
|
558
|
+
* 1) Blocks with the same Tool if tool provides merge method
|
|
559
|
+
*/
|
|
560
|
+
const canMergeBlocksDirectly = targetBlock.name === blockToMerge.name && targetBlock.mergeable;
|
|
561
|
+
const blockToMergeDataRaw = canMergeBlocksDirectly ? await blockToMerge.data : undefined;
|
|
562
|
+
|
|
563
|
+
if (canMergeBlocksDirectly && isEmpty(blockToMergeDataRaw)) {
|
|
564
|
+
console.error('Could not merge Block. Failed to extract original Block data.');
|
|
565
|
+
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (canMergeBlocksDirectly && blockToMergeDataRaw !== undefined) {
|
|
570
|
+
const [ cleanBlock ] = sanitizeBlocks(
|
|
571
|
+
[ { data: blockToMergeDataRaw,
|
|
572
|
+
tool: blockToMerge.name } ],
|
|
573
|
+
targetBlock.tool.sanitizeConfig,
|
|
574
|
+
this.config.sanitizer as SanitizerConfig
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
await completeMerge(cleanBlock.data);
|
|
578
|
+
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 2) Blocks with different Tools if they provides conversionConfig
|
|
584
|
+
*/
|
|
585
|
+
if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) {
|
|
586
|
+
const blockToMergeDataStringified = await blockToMerge.exportDataAsString();
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Extract the field-specific sanitize rules for the field that will receive the imported content.
|
|
590
|
+
*/
|
|
591
|
+
const importProp = targetBlock.tool.conversionConfig?.import;
|
|
592
|
+
const fieldSanitizeConfig = isString(importProp) && isObject(targetBlock.tool.sanitizeConfig[importProp])
|
|
593
|
+
? targetBlock.tool.sanitizeConfig[importProp] as SanitizerConfig
|
|
594
|
+
: targetBlock.tool.sanitizeConfig;
|
|
595
|
+
|
|
596
|
+
const cleanData = clean(blockToMergeDataStringified, fieldSanitizeConfig);
|
|
597
|
+
const blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig);
|
|
598
|
+
|
|
599
|
+
await completeMerge(blockToMergeData);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Remove passed Block
|
|
605
|
+
* @param block - Block to remove
|
|
606
|
+
* @param addLastBlock - if true, adds new default block at the end. @todo remove this logic and use event-bus instead
|
|
607
|
+
*/
|
|
608
|
+
public removeBlock(block: Block, addLastBlock = true): Promise<void> {
|
|
609
|
+
return new Promise((resolve) => {
|
|
610
|
+
const index = this.blocksStore.indexOf(block);
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* If index is not passed and there is no block selected, show a warning
|
|
614
|
+
*/
|
|
615
|
+
if (!this.validateIndex(index)) {
|
|
616
|
+
throw new Error('Can\'t find a Block to remove');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.blocksStore.remove(index);
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Force call of didMutated event on Block removal
|
|
623
|
+
*/
|
|
624
|
+
this.blockDidMutated(BlockRemovedMutationType, block, {
|
|
625
|
+
index,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (this.currentBlockIndex >= index) {
|
|
629
|
+
this.currentBlockIndex--;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* If first Block was removed, insert new Initial Block and set focus on it`s first input
|
|
634
|
+
*/
|
|
635
|
+
const noBlocksLeft = this.blocks.length === 0;
|
|
636
|
+
|
|
637
|
+
if (noBlocksLeft) {
|
|
638
|
+
this.unsetCurrentBlock();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (noBlocksLeft && addLastBlock) {
|
|
642
|
+
this.insert();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!noBlocksLeft && index === 0) {
|
|
646
|
+
this.currentBlockIndex = 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
resolve();
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Remove only selected Blocks
|
|
655
|
+
* and returns first Block index where started removing...
|
|
656
|
+
* @returns {number|undefined}
|
|
657
|
+
*/
|
|
658
|
+
public removeSelectedBlocks(): number | undefined {
|
|
659
|
+
const selectedBlockEntries = this.blocks
|
|
660
|
+
.map((block, index) => ({
|
|
661
|
+
block,
|
|
662
|
+
index,
|
|
663
|
+
}))
|
|
664
|
+
.filter(({ block }) => block.selected)
|
|
665
|
+
.sort((first, second) => second.index - first.index);
|
|
666
|
+
|
|
667
|
+
selectedBlockEntries.forEach(({ block }) => {
|
|
668
|
+
void this.removeBlock(block, false);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return selectedBlockEntries.length > 0
|
|
672
|
+
? selectedBlockEntries[selectedBlockEntries.length - 1].index
|
|
673
|
+
: undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Attention!
|
|
678
|
+
* After removing insert the new default typed Block and focus on it
|
|
679
|
+
* Removes all blocks
|
|
680
|
+
*/
|
|
681
|
+
public removeAllBlocks(): void {
|
|
682
|
+
const removeBlockByIndex = (index: number): void => {
|
|
683
|
+
if (index < 0) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
this.blocksStore.remove(index);
|
|
688
|
+
removeBlockByIndex(index - 1);
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
removeBlockByIndex(this.blocksStore.length - 1);
|
|
692
|
+
|
|
693
|
+
this.unsetCurrentBlock();
|
|
694
|
+
this.insert();
|
|
695
|
+
const currentBlock = this.currentBlock;
|
|
696
|
+
const firstInput = currentBlock?.firstInput;
|
|
697
|
+
|
|
698
|
+
if (firstInput !== undefined) {
|
|
699
|
+
firstInput.focus();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Split current Block
|
|
705
|
+
* 1. Extract content from Caret position to the Block`s end
|
|
706
|
+
* 2. Insert a new Block below current one with extracted content
|
|
707
|
+
* @returns {Block}
|
|
708
|
+
*/
|
|
709
|
+
public split(): Block {
|
|
710
|
+
const extractedFragment = this.Blok.Caret.extractFragmentFromCaretPosition();
|
|
711
|
+
const wrapper = $.make('div');
|
|
712
|
+
|
|
713
|
+
wrapper.appendChild(extractedFragment as DocumentFragment);
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* @todo make object in accordance with Tool
|
|
717
|
+
*/
|
|
718
|
+
const data = {
|
|
719
|
+
text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Renew current Block
|
|
724
|
+
* @type {Block}
|
|
725
|
+
*/
|
|
726
|
+
return this.insert({ data });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Returns Block by passed index
|
|
731
|
+
*
|
|
732
|
+
* If we pass -1 as index, the last block will be returned
|
|
733
|
+
* There shouldn't be a case when there is no blocks at all — at least one always should exist
|
|
734
|
+
*/
|
|
735
|
+
public getBlockByIndex(index: -1): Block;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Returns Block by passed index.
|
|
739
|
+
*
|
|
740
|
+
* Could return undefined if there is no block with such index
|
|
741
|
+
*/
|
|
742
|
+
public getBlockByIndex(index: number): Block | undefined;
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Returns Block by passed index
|
|
746
|
+
* @param {number} index - index to get. -1 to get last
|
|
747
|
+
* @returns {Block}
|
|
748
|
+
*/
|
|
749
|
+
public getBlockByIndex(index: number): Block | undefined {
|
|
750
|
+
const targetIndex = index === -1
|
|
751
|
+
? this.blocksStore.length - 1
|
|
752
|
+
: index;
|
|
753
|
+
|
|
754
|
+
return this.blocksStore[targetIndex];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Returns an index for passed Block
|
|
759
|
+
* @param block - block to find index
|
|
760
|
+
*/
|
|
761
|
+
public getBlockIndex(block: Block): number {
|
|
762
|
+
return this.blocksStore.indexOf(block);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Returns the Block by passed id
|
|
767
|
+
* @param id - id of block to get
|
|
768
|
+
* @returns {Block}
|
|
769
|
+
*/
|
|
770
|
+
public getBlockById(id: string): Block | undefined {
|
|
771
|
+
return this.blocksStore.array.find((block) => block.id === id);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Returns the depth (nesting level) of a block in the hierarchy.
|
|
776
|
+
* Root-level blocks have depth 0.
|
|
777
|
+
* @param block - the block to get depth for
|
|
778
|
+
* @returns {number} - depth level (0 for root, 1 for first level children, etc.)
|
|
779
|
+
*/
|
|
780
|
+
public getBlockDepth(block: Block): number {
|
|
781
|
+
const calculateDepth = (parentId: string | null, currentDepth: number): number => {
|
|
782
|
+
if (parentId === null) {
|
|
783
|
+
return currentDepth;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const parentBlock = this.getBlockById(parentId);
|
|
787
|
+
|
|
788
|
+
if (parentBlock === undefined) {
|
|
789
|
+
return currentDepth;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return calculateDepth(parentBlock.parentId, currentDepth + 1);
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
return calculateDepth(block.parentId, 0);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Sets the parent of a block, updating both the block's parentId and the parent's contentIds.
|
|
800
|
+
* @param block - the block to reparent
|
|
801
|
+
* @param newParentId - the new parent block id, or null for root level
|
|
802
|
+
*/
|
|
803
|
+
public setBlockParent(block: Block, newParentId: string | null): void {
|
|
804
|
+
const oldParentId = block.parentId;
|
|
805
|
+
|
|
806
|
+
// Remove from old parent's contentIds
|
|
807
|
+
const oldParent = oldParentId !== null ? this.getBlockById(oldParentId) : undefined;
|
|
808
|
+
|
|
809
|
+
if (oldParent !== undefined) {
|
|
810
|
+
oldParent.contentIds = oldParent.contentIds.filter(id => id !== block.id);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Add to new parent's contentIds
|
|
814
|
+
const newParent = newParentId !== null ? this.getBlockById(newParentId) : undefined;
|
|
815
|
+
const shouldAddToNewParent = newParent !== undefined && !newParent.contentIds.includes(block.id);
|
|
816
|
+
|
|
817
|
+
if (shouldAddToNewParent) {
|
|
818
|
+
newParent.contentIds.push(block.id);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Update block's parentId - parentId is a public mutable property on Block
|
|
822
|
+
// eslint-disable-next-line no-param-reassign
|
|
823
|
+
block.parentId = newParentId;
|
|
824
|
+
|
|
825
|
+
// Update visual indentation
|
|
826
|
+
this.updateBlockIndentation(block);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Updates the visual indentation of a block based on its depth in the hierarchy.
|
|
831
|
+
* @param block - the block to update indentation for
|
|
832
|
+
*/
|
|
833
|
+
public updateBlockIndentation(block: Block): void {
|
|
834
|
+
const depth = this.getBlockDepth(block);
|
|
835
|
+
const indentationPx = depth * 24; // 24px per level
|
|
836
|
+
const { holder } = block;
|
|
837
|
+
|
|
838
|
+
holder.style.marginLeft = indentationPx > 0 ? `${indentationPx}px` : '';
|
|
839
|
+
holder.setAttribute('data-blok-depth', depth.toString());
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Get Block instance by html element
|
|
844
|
+
* @param {Node} element - html element to get Block by
|
|
845
|
+
*/
|
|
846
|
+
public getBlock(element: HTMLElement): Block | undefined {
|
|
847
|
+
const normalizedElement = (($.isElement(element) as boolean) ? element : element.parentNode) as HTMLElement | null;
|
|
848
|
+
|
|
849
|
+
if (!normalizedElement) {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const nodes = this.blocksStore.nodes;
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
const firstLevelBlock = normalizedElement.closest(createSelector(DATA_ATTR.element));
|
|
857
|
+
|
|
858
|
+
if (!firstLevelBlock) {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const index = nodes.indexOf(firstLevelBlock as HTMLElement);
|
|
863
|
+
|
|
864
|
+
if (index >= 0) {
|
|
865
|
+
return this.blocksStore[index];
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 1) Find first-level Block from passed child Node
|
|
873
|
+
* 2) Mark it as current
|
|
874
|
+
* @param {Node} childNode - look ahead from this node.
|
|
875
|
+
* @returns {Block | undefined} can return undefined in case when the passed child note is not a part of the current blok instance
|
|
876
|
+
*/
|
|
877
|
+
public setCurrentBlockByChildNode(childNode: Node): Block | undefined {
|
|
878
|
+
/**
|
|
879
|
+
* If node is Text TextNode
|
|
880
|
+
*/
|
|
881
|
+
const normalizedChildNode = ($.isElement(childNode) ? childNode : childNode.parentNode) as HTMLElement | null;
|
|
882
|
+
|
|
883
|
+
if (!normalizedChildNode) {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const parentFirstLevelBlock = normalizedChildNode.closest(createSelector(DATA_ATTR.element));
|
|
888
|
+
|
|
889
|
+
if (!parentFirstLevelBlock) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Support multiple Blok instances,
|
|
895
|
+
* by checking whether the found block belongs to the current instance
|
|
896
|
+
* @see {@link Ui#documentTouched}
|
|
897
|
+
*/
|
|
898
|
+
const blokWrapper = parentFirstLevelBlock.closest(createSelector(DATA_ATTR.editor));
|
|
899
|
+
const isBlockBelongsToCurrentInstance = blokWrapper?.isEqualNode(this.Blok.UI.nodes.wrapper);
|
|
900
|
+
|
|
901
|
+
if (!isBlockBelongsToCurrentInstance) {
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Update current Block's index
|
|
907
|
+
* @type {number}
|
|
908
|
+
*/
|
|
909
|
+
if (!(parentFirstLevelBlock instanceof HTMLElement)) {
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
this.currentBlockIndex = this.blocksStore.nodes.indexOf(parentFirstLevelBlock);
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Update current block active input
|
|
917
|
+
*/
|
|
918
|
+
const currentBlock = this.currentBlock;
|
|
919
|
+
|
|
920
|
+
currentBlock?.updateCurrentInput();
|
|
921
|
+
|
|
922
|
+
return currentBlock;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Return block which contents passed node
|
|
927
|
+
* @param {Node} childNode - node to get Block by
|
|
928
|
+
* @returns {Block}
|
|
929
|
+
*/
|
|
930
|
+
public getBlockByChildNode(childNode: Node): Block | undefined {
|
|
931
|
+
if (!(childNode instanceof Node)) {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* If node is Text TextNode
|
|
937
|
+
*/
|
|
938
|
+
const normalizedChildNode = ($.isElement(childNode) ? childNode : childNode.parentNode) as HTMLElement | null;
|
|
939
|
+
|
|
940
|
+
if (!normalizedChildNode) {
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
const firstLevelBlock = normalizedChildNode.closest(createSelector(DATA_ATTR.element));
|
|
946
|
+
|
|
947
|
+
if (!firstLevelBlock) {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return this.blocks.find((block) => block.holder === firstLevelBlock);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Move a block to a new index
|
|
956
|
+
* @param {number} toIndex - index where to move Block
|
|
957
|
+
* @param {number} fromIndex - index of Block to move
|
|
958
|
+
* @param {boolean} skipDOM - if true, do not manipulate DOM
|
|
959
|
+
*/
|
|
960
|
+
public move(toIndex: number, fromIndex: number = this.currentBlockIndex, skipDOM = false): void {
|
|
961
|
+
// make sure indexes are valid and within a valid range
|
|
962
|
+
if (isNaN(toIndex) || isNaN(fromIndex)) {
|
|
963
|
+
log(`Warning during 'move' call: incorrect indices provided.`, 'warn');
|
|
964
|
+
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (!this.validateIndex(toIndex) || !this.validateIndex(fromIndex)) {
|
|
969
|
+
log(`Warning during 'move' call: indices cannot be lower than 0 or greater than the amount of blocks.`, 'warn');
|
|
970
|
+
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** Move up current Block */
|
|
975
|
+
this.blocksStore.move(toIndex, fromIndex, skipDOM);
|
|
976
|
+
|
|
977
|
+
/** Now actual block moved so that current block index changed */
|
|
978
|
+
this.currentBlockIndex = toIndex;
|
|
979
|
+
const movedBlock = this.currentBlock;
|
|
980
|
+
|
|
981
|
+
if (movedBlock === undefined) {
|
|
982
|
+
throw new Error(`Could not move Block. Block at index ${toIndex} is not available.`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Force call of didMutated event on Block movement
|
|
987
|
+
*/
|
|
988
|
+
this.blockDidMutated(BlockMovedMutationType, movedBlock, {
|
|
989
|
+
fromIndex,
|
|
990
|
+
toIndex,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Converts passed Block to the new Tool
|
|
996
|
+
* Uses Conversion Config
|
|
997
|
+
* @param blockToConvert - Block that should be converted
|
|
998
|
+
* @param targetToolName - name of the Tool to convert to
|
|
999
|
+
* @param blockDataOverrides - optional new Block data overrides
|
|
1000
|
+
*/
|
|
1001
|
+
public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<Block> {
|
|
1002
|
+
/**
|
|
1003
|
+
* At first, we get current Block data
|
|
1004
|
+
*/
|
|
1005
|
+
const savedBlock = await blockToConvert.save();
|
|
1006
|
+
|
|
1007
|
+
if (!savedBlock || savedBlock.data === undefined) {
|
|
1008
|
+
throw new Error('Could not convert Block. Failed to extract original Block data.');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Getting a class of the replacing Tool
|
|
1013
|
+
*/
|
|
1014
|
+
const replacingTool = this.Blok.Tools.blockTools.get(targetToolName);
|
|
1015
|
+
|
|
1016
|
+
if (!replacingTool) {
|
|
1017
|
+
throw new Error(`Could not convert Block. Tool «${targetToolName}» not found.`);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Using Conversion Config "export" we get a stringified version of the Block data
|
|
1022
|
+
*/
|
|
1023
|
+
const exportedData = await blockToConvert.exportDataAsString();
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Clean exported data with replacing sanitizer config.
|
|
1027
|
+
* We need to extract the field-specific sanitize rules for the field that will receive the imported content.
|
|
1028
|
+
* The tool's sanitizeConfig has the format { fieldName: { tagRules } }, but clean() expects just { tagRules }.
|
|
1029
|
+
*/
|
|
1030
|
+
const importProp = replacingTool.conversionConfig?.import;
|
|
1031
|
+
const fieldSanitizeConfig = isString(importProp) && isObject(replacingTool.sanitizeConfig[importProp])
|
|
1032
|
+
? replacingTool.sanitizeConfig[importProp] as SanitizerConfig
|
|
1033
|
+
: replacingTool.sanitizeConfig;
|
|
1034
|
+
|
|
1035
|
+
const cleanData: string = clean(
|
|
1036
|
+
exportedData,
|
|
1037
|
+
composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, fieldSanitizeConfig)
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Now using Conversion Config "import" we compose a new Block data
|
|
1042
|
+
*/
|
|
1043
|
+
const baseBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings);
|
|
1044
|
+
|
|
1045
|
+
const newBlockData = blockDataOverrides
|
|
1046
|
+
? Object.assign(baseBlockData, blockDataOverrides)
|
|
1047
|
+
: baseBlockData;
|
|
1048
|
+
|
|
1049
|
+
return this.replace(blockToConvert, replacingTool.name, newBlockData);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Sets current Block Index -1 which means unknown
|
|
1054
|
+
* and clear highlights
|
|
1055
|
+
*/
|
|
1056
|
+
public unsetCurrentBlock(): void {
|
|
1057
|
+
this.currentBlockIndex = -1;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Clears Blok
|
|
1062
|
+
* @param {boolean} needToAddDefaultBlock - 1) in internal calls (for example, in api.blocks.render)
|
|
1063
|
+
* we don't need to add an empty default block
|
|
1064
|
+
* 2) in api.blocks.clear we should add empty block
|
|
1065
|
+
*/
|
|
1066
|
+
public async clear(needToAddDefaultBlock = false): Promise<void> {
|
|
1067
|
+
const queue = new PromiseQueue();
|
|
1068
|
+
|
|
1069
|
+
// Create a copy of the blocks array to avoid issues with array modification during iteration
|
|
1070
|
+
const blocksToRemove = [ ...this.blocks ];
|
|
1071
|
+
|
|
1072
|
+
blocksToRemove.forEach((block) => {
|
|
1073
|
+
void queue.add(async () => {
|
|
1074
|
+
await this.removeBlock(block, false);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
await queue.completed;
|
|
1079
|
+
|
|
1080
|
+
this.unsetCurrentBlock();
|
|
1081
|
+
|
|
1082
|
+
if (needToAddDefaultBlock) {
|
|
1083
|
+
this.insert();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Add empty modifier
|
|
1088
|
+
*/
|
|
1089
|
+
this.Blok.UI.checkEmptiness();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Moves the current block up by one position
|
|
1094
|
+
* Does nothing if the block is already at the top
|
|
1095
|
+
*/
|
|
1096
|
+
public moveCurrentBlockUp(): void {
|
|
1097
|
+
const currentIndex = this.currentBlockIndex;
|
|
1098
|
+
|
|
1099
|
+
if (currentIndex <= 0) {
|
|
1100
|
+
// Announce boundary condition
|
|
1101
|
+
announce(
|
|
1102
|
+
this.Blok.I18n.t('a11y.atTop'),
|
|
1103
|
+
{ politeness: 'polite' }
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
this.move(currentIndex - 1, currentIndex);
|
|
1110
|
+
this.refocusCurrentBlock();
|
|
1111
|
+
|
|
1112
|
+
// Announce successful move (currentBlockIndex is now updated to new position)
|
|
1113
|
+
const newPosition = this.currentBlockIndex + 1; // Convert to 1-indexed for user
|
|
1114
|
+
const total = this.blocksStore.length;
|
|
1115
|
+
const message = this.Blok.I18n.t('a11y.movedUp', {
|
|
1116
|
+
position: newPosition,
|
|
1117
|
+
total,
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
announce(message, { politeness: 'assertive' });
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Moves the current block down by one position
|
|
1125
|
+
* Does nothing if the block is already at the bottom
|
|
1126
|
+
*/
|
|
1127
|
+
public moveCurrentBlockDown(): void {
|
|
1128
|
+
const currentIndex = this.currentBlockIndex;
|
|
1129
|
+
|
|
1130
|
+
if (currentIndex < 0 || currentIndex >= this.blocksStore.length - 1) {
|
|
1131
|
+
// Announce boundary condition
|
|
1132
|
+
announce(
|
|
1133
|
+
this.Blok.I18n.t('a11y.atBottom'),
|
|
1134
|
+
{ politeness: 'polite' }
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
this.move(currentIndex + 1, currentIndex);
|
|
1141
|
+
this.refocusCurrentBlock();
|
|
1142
|
+
|
|
1143
|
+
// Announce successful move (currentBlockIndex is now updated to new position)
|
|
1144
|
+
const newPosition = this.currentBlockIndex + 1; // Convert to 1-indexed for user
|
|
1145
|
+
const total = this.blocksStore.length;
|
|
1146
|
+
const message = this.Blok.I18n.t('a11y.movedDown', {
|
|
1147
|
+
position: newPosition,
|
|
1148
|
+
total,
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
announce(message, { politeness: 'assertive' });
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Refocuses the current block at the end position
|
|
1156
|
+
* Used after block movement to allow consecutive moves
|
|
1157
|
+
*/
|
|
1158
|
+
private refocusCurrentBlock(): void {
|
|
1159
|
+
const block = this.currentBlock;
|
|
1160
|
+
|
|
1161
|
+
if (block !== undefined) {
|
|
1162
|
+
this.Blok.Caret.setToBlock(block, this.Blok.Caret.positions.END);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Sets up keyboard shortcuts for block movement
|
|
1168
|
+
* CMD+SHIFT+UP: Move current block up
|
|
1169
|
+
* CMD+SHIFT+DOWN: Move current block down
|
|
1170
|
+
*/
|
|
1171
|
+
private setupKeyboardShortcuts(): void {
|
|
1172
|
+
// Wait for UI to be ready (same pattern as History module)
|
|
1173
|
+
setTimeout(() => {
|
|
1174
|
+
const shortcutNames = ['CMD+SHIFT+UP', 'CMD+SHIFT+DOWN'];
|
|
1175
|
+
|
|
1176
|
+
// Clear any existing shortcuts to avoid duplicate registration errors
|
|
1177
|
+
shortcutNames.forEach(name => Shortcuts.remove(document, name));
|
|
1178
|
+
|
|
1179
|
+
// Move block up: Cmd+Shift+ArrowUp (Mac) / Ctrl+Shift+ArrowUp (Windows/Linux)
|
|
1180
|
+
Shortcuts.add({
|
|
1181
|
+
name: 'CMD+SHIFT+UP',
|
|
1182
|
+
on: document,
|
|
1183
|
+
handler: (event: KeyboardEvent) => {
|
|
1184
|
+
if (!this.shouldHandleShortcut(event)) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
this.moveCurrentBlockUp();
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
this.registeredShortcuts.push('CMD+SHIFT+UP');
|
|
1192
|
+
|
|
1193
|
+
// Move block down: Cmd+Shift+ArrowDown (Mac) / Ctrl+Shift+ArrowDown (Windows/Linux)
|
|
1194
|
+
Shortcuts.add({
|
|
1195
|
+
name: 'CMD+SHIFT+DOWN',
|
|
1196
|
+
on: document,
|
|
1197
|
+
handler: (event: KeyboardEvent) => {
|
|
1198
|
+
if (!this.shouldHandleShortcut(event)) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
this.moveCurrentBlockDown();
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
this.registeredShortcuts.push('CMD+SHIFT+DOWN');
|
|
1206
|
+
}, 0);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Determines whether the block movement shortcut should be handled
|
|
1211
|
+
* Only handles shortcuts when focus is inside the editor
|
|
1212
|
+
* @param event - the keyboard event
|
|
1213
|
+
* @returns true if the shortcut should be handled
|
|
1214
|
+
*/
|
|
1215
|
+
private shouldHandleShortcut(event: KeyboardEvent): boolean {
|
|
1216
|
+
const target = event.target;
|
|
1217
|
+
|
|
1218
|
+
return target instanceof HTMLElement &&
|
|
1219
|
+
this.Blok.UI?.nodes?.wrapper?.contains(target) === true;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Cleans up all the block tools' resources
|
|
1224
|
+
* This is called when blok is destroyed
|
|
1225
|
+
*/
|
|
1226
|
+
public async destroy(): Promise<void> {
|
|
1227
|
+
// Remove registered keyboard shortcuts
|
|
1228
|
+
for (const name of this.registeredShortcuts) {
|
|
1229
|
+
Shortcuts.remove(document, name);
|
|
1230
|
+
}
|
|
1231
|
+
this.registeredShortcuts = [];
|
|
1232
|
+
|
|
1233
|
+
await Promise.all(this.blocks.map((block) => {
|
|
1234
|
+
return block.destroy();
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Bind Block events
|
|
1240
|
+
* @param {Block} block - Block to which event should be bound
|
|
1241
|
+
*/
|
|
1242
|
+
private bindBlockEvents(block: Block): void {
|
|
1243
|
+
const { BlockEvents } = this.Blok;
|
|
1244
|
+
|
|
1245
|
+
this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: Event) => {
|
|
1246
|
+
if (event instanceof KeyboardEvent) {
|
|
1247
|
+
BlockEvents.keydown(event);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: Event) => {
|
|
1252
|
+
if (event instanceof KeyboardEvent) {
|
|
1253
|
+
BlockEvents.keyup(event);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
this.readOnlyMutableListeners.on(block.holder, 'input', (event: Event) => {
|
|
1258
|
+
if (event instanceof InputEvent) {
|
|
1259
|
+
BlockEvents.input(event);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
block.on('didMutated', (affectedBlock: Block) => {
|
|
1264
|
+
return this.blockDidMutated(BlockChangedMutationType, affectedBlock, {
|
|
1265
|
+
index: this.getBlockIndex(affectedBlock),
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Disable mutable handlers and bindings
|
|
1272
|
+
*/
|
|
1273
|
+
private disableModuleBindings(): void {
|
|
1274
|
+
this.readOnlyMutableListeners.clearAll();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Enables all module handlers and bindings for all Blocks
|
|
1279
|
+
*/
|
|
1280
|
+
private enableModuleBindings(): void {
|
|
1281
|
+
/** Cut event */
|
|
1282
|
+
this.readOnlyMutableListeners.on(
|
|
1283
|
+
document,
|
|
1284
|
+
'cut',
|
|
1285
|
+
(event: Event) => {
|
|
1286
|
+
this.Blok.BlockEvents.handleCommandX(event as ClipboardEvent);
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
this.blocks.forEach((block: Block) => {
|
|
1291
|
+
this.bindBlockEvents(block);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Validates that the given index is not lower than 0 or higher than the amount of blocks
|
|
1297
|
+
* @param {number} index - index of blocks array to validate
|
|
1298
|
+
* @returns {boolean}
|
|
1299
|
+
*/
|
|
1300
|
+
private validateIndex(index: number): boolean {
|
|
1301
|
+
return !(index < 0 || index >= this.blocksStore.length);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Block mutation callback
|
|
1306
|
+
* @param mutationType - what happened with block
|
|
1307
|
+
* @param block - mutated block
|
|
1308
|
+
* @param detailData - additional data to pass with change event
|
|
1309
|
+
*/
|
|
1310
|
+
private blockDidMutated<Type extends BlockMutationType>(mutationType: Type, block: Block, detailData: BlockMutationEventDetailWithoutTarget<Type>): Block {
|
|
1311
|
+
const eventDetail = {
|
|
1312
|
+
target: new BlockAPI(block),
|
|
1313
|
+
...detailData as BlockMutationEventDetailWithoutTarget<Type>,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const event = new CustomEvent(mutationType, {
|
|
1317
|
+
detail: {
|
|
1318
|
+
...eventDetail,
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* The CustomEvent#type getter is not enumerable by default, so it gets lost during structured cloning.
|
|
1324
|
+
* Define it explicitly to keep the type available for consumers like Playwright tests.
|
|
1325
|
+
*/
|
|
1326
|
+
if (!Object.prototype.propertyIsEnumerable.call(event, 'type')) {
|
|
1327
|
+
Object.defineProperty(event, 'type', {
|
|
1328
|
+
value: mutationType,
|
|
1329
|
+
enumerable: true,
|
|
1330
|
+
configurable: true,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* CustomEvent#detail is also non-enumerable, so preserve it for consumers outside of the browser context.
|
|
1336
|
+
*/
|
|
1337
|
+
if (!Object.prototype.propertyIsEnumerable.call(event, 'detail')) {
|
|
1338
|
+
Object.defineProperty(event, 'detail', {
|
|
1339
|
+
value: eventDetail,
|
|
1340
|
+
enumerable: true,
|
|
1341
|
+
configurable: true,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
this.eventsDispatcher.emit(BlockChanged, {
|
|
1346
|
+
event: event as BlockMutationEventMap[Type],
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
return block;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Type alias for Block Mutation event without 'target' field, used in 'blockDidMutated' method
|
|
1355
|
+
*/
|
|
1356
|
+
type BlockMutationEventDetailWithoutTarget<Type extends BlockMutationType> = Omit<BlockMutationEventMap[Type]['detail'], 'target'>;
|