@jackuait/blok 0.4.1-beta.0 → 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-OwEtDFlk.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 +30 -13
- package/types/locales.d.ts +59 -0
- package/types/tools/adapters/inline-tool-adapter.d.ts +10 -0
- package/types/tools/block-tool.d.ts +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-D_baBvTG.mjs +0 -25795
- package/dist/blok.umd.js +0 -181
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockAPI as BlockAPIInterface,
|
|
3
|
+
BlockTool as IBlockTool,
|
|
4
|
+
BlockToolData,
|
|
5
|
+
BlockTune as IBlockTune,
|
|
6
|
+
SanitizerConfig,
|
|
7
|
+
ToolConfig,
|
|
8
|
+
ToolboxConfigEntry,
|
|
9
|
+
PopoverItemParams
|
|
10
|
+
} from '../../../types';
|
|
11
|
+
|
|
12
|
+
import type { SavedData } from '../../../types/data-formats';
|
|
13
|
+
import { twMerge } from '../utils/tw';
|
|
14
|
+
import { Dom as $, toggleEmptyMark } from '../dom';
|
|
15
|
+
import { generateBlockId, isEmpty, isFunction, log } from '../utils';
|
|
16
|
+
import type { API as ApiModules } from '../modules/api';
|
|
17
|
+
import { BlockAPI } from './api';
|
|
18
|
+
import { SelectionUtils } from '../selection';
|
|
19
|
+
import type { BlockToolAdapter } from '../tools/block';
|
|
20
|
+
|
|
21
|
+
import type { BlockTuneAdapter } from '../tools/tune';
|
|
22
|
+
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
|
23
|
+
import type { ToolsCollection } from '../tools/collection';
|
|
24
|
+
import { EventsDispatcher } from '../utils/events';
|
|
25
|
+
import type { MenuConfigItem } from '../../../types/tools';
|
|
26
|
+
import { isMutationBelongsToElement } from '../utils/mutations';
|
|
27
|
+
import type { BlokEventMap } from '../events';
|
|
28
|
+
import { FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
|
29
|
+
import type { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
|
30
|
+
import { convertBlockDataToString, isSameBlockData } from '../utils/blocks';
|
|
31
|
+
import { PopoverItemType } from '@/types/utils/popover/popover-item-type';
|
|
32
|
+
import { DATA_ATTR, createSelector } from '../constants';
|
|
33
|
+
import type { DragManager } from '../modules/dragManager';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Interface describes Block class constructor argument
|
|
37
|
+
*/
|
|
38
|
+
type BlockSaveResult = SavedData & { tunes: { [name: string]: BlockTuneData } };
|
|
39
|
+
|
|
40
|
+
interface BlockConstructorOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Block's id. Should be passed for existed block, and omitted for a new one.
|
|
43
|
+
*/
|
|
44
|
+
id?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initial Block data
|
|
48
|
+
*/
|
|
49
|
+
data: BlockToolData;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tool object
|
|
53
|
+
*/
|
|
54
|
+
tool: BlockToolAdapter;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Blok's API methods
|
|
58
|
+
*/
|
|
59
|
+
api: ApiModules;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This flag indicates that the Block should be constructed in the read-only mode.
|
|
63
|
+
*/
|
|
64
|
+
readOnly: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Tunes data for current Block
|
|
68
|
+
*/
|
|
69
|
+
tunesData: { [name: string]: BlockTuneData };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parent block id for hierarchical structure (Notion-like flat-with-references model).
|
|
73
|
+
* When present, this block is a child of the block with the specified id.
|
|
74
|
+
*/
|
|
75
|
+
parentId?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Array of child block ids (Notion-like flat-with-references model).
|
|
79
|
+
* References blocks that are children of this block.
|
|
80
|
+
*/
|
|
81
|
+
contentIds?: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @class Block
|
|
86
|
+
* @classdesc This class describes blok`s block, including block`s HTMLElement, data and tool
|
|
87
|
+
* @property {BlockToolAdapter} tool — current block tool (Paragraph, for example)
|
|
88
|
+
* @property {object} CSS — block`s css classes
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Available Block Tool API methods
|
|
93
|
+
*/
|
|
94
|
+
export enum BlockToolAPI {
|
|
95
|
+
RENDERED = 'rendered',
|
|
96
|
+
MOVED = 'moved',
|
|
97
|
+
UPDATED = 'updated',
|
|
98
|
+
REMOVED = 'removed',
|
|
99
|
+
|
|
100
|
+
ON_PASTE = 'onPaste',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Names of events used in Block
|
|
105
|
+
*/
|
|
106
|
+
interface BlockEvents {
|
|
107
|
+
'didMutated': Block,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
|
112
|
+
* @property {BlockToolAdapter} tool - Tool instance
|
|
113
|
+
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content.
|
|
114
|
+
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
|
|
115
|
+
*/
|
|
116
|
+
export class Block extends EventsDispatcher<BlockEvents> {
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tailwind styles for the Block elements
|
|
120
|
+
*/
|
|
121
|
+
private static readonly styles = {
|
|
122
|
+
wrapper: 'relative opacity-100 animate-fade-in my-[-0.5em] py-[0.5em] first:mt-0 [&_a]:cursor-pointer [&_a]:underline [&_a]:text-link [&_b]:font-bold [&_i]:italic',
|
|
123
|
+
content: 'relative mx-auto transition-colors duration-150 ease-out max-w-content',
|
|
124
|
+
contentSelected: 'bg-selection rounded-[4px] [&_[contenteditable]]:select-none [&_img]:opacity-55 [&_[data-blok-tool=stub]]:opacity-55',
|
|
125
|
+
contentStretched: 'max-w-none',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Block unique identifier
|
|
130
|
+
*/
|
|
131
|
+
public id: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parent block id for hierarchical structure (Notion-like flat-with-references model).
|
|
135
|
+
* Null if this is a root-level block.
|
|
136
|
+
*/
|
|
137
|
+
public parentId: string | null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Array of child block ids (Notion-like flat-with-references model).
|
|
141
|
+
* Empty array if block has no children.
|
|
142
|
+
*/
|
|
143
|
+
public contentIds: string[];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Block Tool`s name
|
|
147
|
+
*/
|
|
148
|
+
public readonly name: string;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Instance of the Tool Block represents
|
|
152
|
+
*/
|
|
153
|
+
public readonly tool: BlockToolAdapter;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* User Tool configuration
|
|
157
|
+
*/
|
|
158
|
+
public readonly settings: ToolConfig;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Wrapper for Block`s content
|
|
162
|
+
*/
|
|
163
|
+
public readonly holder: HTMLDivElement;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Tunes used by Tool
|
|
167
|
+
*/
|
|
168
|
+
public readonly tunes: ToolsCollection<BlockTuneAdapter>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Tool's user configuration
|
|
172
|
+
*/
|
|
173
|
+
public readonly config: ToolConfig;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Stores last successfully extracted block data
|
|
177
|
+
*/
|
|
178
|
+
private lastSavedData: BlockToolData;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Cached inputs
|
|
182
|
+
*/
|
|
183
|
+
private cachedInputs: HTMLElement[] = [];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stores last successfully extracted tunes data
|
|
187
|
+
*/
|
|
188
|
+
private lastSavedTunes: { [name: string]: BlockTuneData } = {};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* We'll store a reference to the tool's rendered element to access it later
|
|
192
|
+
*/
|
|
193
|
+
private toolRenderedElement: HTMLElement | null = null;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Reference to the content wrapper element for style toggling
|
|
197
|
+
*/
|
|
198
|
+
private contentElement: HTMLElement | null = null;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Tool class instance
|
|
202
|
+
*/
|
|
203
|
+
private readonly toolInstance: IBlockTool;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* User provided Block Tunes instances
|
|
207
|
+
*/
|
|
208
|
+
private readonly tunesInstances: Map<string, IBlockTune> = new Map();
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Blok provided Block Tunes instances
|
|
212
|
+
*/
|
|
213
|
+
private readonly defaultTunesInstances: Map<string, IBlockTune> = new Map();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Promise that resolves when the block is ready (rendered)
|
|
217
|
+
*/
|
|
218
|
+
public ready: Promise<void>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resolver for ready promise
|
|
222
|
+
*/
|
|
223
|
+
private readyResolver: (() => void) | null = null;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* If there is saved data for Tune which is not available at the moment,
|
|
227
|
+
* we will store it here and provide back on save so data is not lost
|
|
228
|
+
*/
|
|
229
|
+
private unavailableTunesData: { [name: string]: BlockTuneData } = {};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Focused input index
|
|
233
|
+
* @type {number}
|
|
234
|
+
*/
|
|
235
|
+
private inputIndex = 0;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Common blok event bus
|
|
239
|
+
*/
|
|
240
|
+
private readonly blokEventBus: EventsDispatcher<BlokEventMap> | null = null;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Current block API interface
|
|
244
|
+
*/
|
|
245
|
+
private readonly blockAPI: BlockAPIInterface;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Cleanup function for draggable behavior
|
|
249
|
+
*/
|
|
250
|
+
private draggableCleanup: (() => void) | null = null;
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param options - block constructor options
|
|
255
|
+
* @param [options.id] - block's id. Will be generated if omitted.
|
|
256
|
+
* @param options.data - Tool's initial data
|
|
257
|
+
* @param options.tool — block's tool
|
|
258
|
+
* @param options.api - Blok API module for pass it to the Block Tunes
|
|
259
|
+
* @param options.readOnly - Read-Only flag
|
|
260
|
+
* @param [options.parentId] - parent block id for hierarchical structure
|
|
261
|
+
* @param [options.contentIds] - array of child block ids
|
|
262
|
+
* @param [eventBus] - Blok common event bus. Allows to subscribe on some Blok events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData.
|
|
263
|
+
*/
|
|
264
|
+
constructor({
|
|
265
|
+
id = generateBlockId(),
|
|
266
|
+
data,
|
|
267
|
+
tool,
|
|
268
|
+
readOnly,
|
|
269
|
+
tunesData,
|
|
270
|
+
parentId,
|
|
271
|
+
contentIds,
|
|
272
|
+
}: BlockConstructorOptions, eventBus?: EventsDispatcher<BlokEventMap>) {
|
|
273
|
+
super();
|
|
274
|
+
this.ready = new Promise((resolve) => {
|
|
275
|
+
this.readyResolver = resolve;
|
|
276
|
+
});
|
|
277
|
+
this.name = tool.name;
|
|
278
|
+
this.id = id;
|
|
279
|
+
this.parentId = parentId ?? null;
|
|
280
|
+
this.contentIds = contentIds ?? [];
|
|
281
|
+
this.settings = tool.settings;
|
|
282
|
+
this.config = this.settings;
|
|
283
|
+
this.blokEventBus = eventBus || null;
|
|
284
|
+
this.blockAPI = new BlockAPI(this);
|
|
285
|
+
this.lastSavedData = data ?? {};
|
|
286
|
+
this.lastSavedTunes = tunesData ?? {};
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
this.tool = tool;
|
|
290
|
+
this.toolInstance = tool.create(data, this.blockAPI, readOnly);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @type {BlockTuneAdapter[]}
|
|
294
|
+
*/
|
|
295
|
+
this.tunes = tool.tunes;
|
|
296
|
+
|
|
297
|
+
this.composeTunes(tunesData);
|
|
298
|
+
|
|
299
|
+
const holderElement = this.compose();
|
|
300
|
+
|
|
301
|
+
if (holderElement == null) {
|
|
302
|
+
throw new Error(`Tool "${this.name}" did not return a block holder element during render()`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.holder = holderElement;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Bind block events in RIC for optimizing of constructing process time
|
|
309
|
+
*/
|
|
310
|
+
window.requestIdleCallback(() => {
|
|
311
|
+
/**
|
|
312
|
+
* Start watching block mutations
|
|
313
|
+
*/
|
|
314
|
+
this.watchBlockMutations();
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
|
|
318
|
+
* so we need to track focus events to update current input and clear cache.
|
|
319
|
+
*/
|
|
320
|
+
this.addInputEvents();
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* We mark inputs with [data-blok-empty] attribute
|
|
324
|
+
* It can be useful for developers, for example for correct placeholder behavior
|
|
325
|
+
*/
|
|
326
|
+
this.toggleInputsEmptyMark();
|
|
327
|
+
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Makes this block draggable using the provided drag handle element
|
|
333
|
+
* Called by the toolbar when it moves to this block
|
|
334
|
+
* @param dragHandle - The element to use as the drag handle
|
|
335
|
+
* @param dragManager - DragManager instance to handle drag operations
|
|
336
|
+
*/
|
|
337
|
+
public setupDraggable(dragHandle: HTMLElement, dragManager: DragManager): void {
|
|
338
|
+
/** Clean up any existing draggable */
|
|
339
|
+
this.cleanupDraggable();
|
|
340
|
+
|
|
341
|
+
/** Set up drag handling via DragManager (pointer-based, not native HTML5 drag) */
|
|
342
|
+
this.draggableCleanup = dragManager.setupDragHandle(dragHandle, this);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Cleans up the draggable behavior
|
|
347
|
+
* Called when the toolbar moves away from this block
|
|
348
|
+
*/
|
|
349
|
+
public cleanupDraggable(): void {
|
|
350
|
+
if (this.draggableCleanup) {
|
|
351
|
+
this.draggableCleanup();
|
|
352
|
+
this.draggableCleanup = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Calls Tool's method
|
|
358
|
+
*
|
|
359
|
+
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
|
|
360
|
+
* @param {string} methodName - method to call
|
|
361
|
+
* @param {object} params - method argument
|
|
362
|
+
*/
|
|
363
|
+
public call(methodName: string, params?: object): void {
|
|
364
|
+
/**
|
|
365
|
+
* call Tool's method with the instance context
|
|
366
|
+
*/
|
|
367
|
+
const method = (this.toolInstance as unknown as Record<string, unknown>)[methodName];
|
|
368
|
+
|
|
369
|
+
if (!isFunction(method)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
|
|
375
|
+
method.call(this.toolInstance, params);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
378
|
+
|
|
379
|
+
log(`Error during '${methodName}' call: ${errorMessage}`, 'error');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Call plugins merge method
|
|
385
|
+
* @param {BlockToolData} data - data to merge
|
|
386
|
+
*/
|
|
387
|
+
public async mergeWith(data: BlockToolData): Promise<void> {
|
|
388
|
+
if (!isFunction(this.toolInstance.merge)) {
|
|
389
|
+
throw new Error(`Block tool "${this.name}" does not support merging`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await this.toolInstance.merge(data);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Returns the horizontal offset of the content at the hovered element.
|
|
397
|
+
* Delegates to the tool's getContentOffset method if implemented.
|
|
398
|
+
*
|
|
399
|
+
* @param hoveredElement - The element that is currently being hovered
|
|
400
|
+
* @returns Object with left offset in pixels, or undefined if no offset should be applied
|
|
401
|
+
*/
|
|
402
|
+
public getContentOffset(hoveredElement: Element): { left: number } | undefined {
|
|
403
|
+
if (typeof this.toolInstance.getContentOffset === 'function') {
|
|
404
|
+
return this.toolInstance.getContentOffset(hoveredElement);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Extracts data from Block
|
|
412
|
+
* Groups Tool's save processing time
|
|
413
|
+
* @returns {object}
|
|
414
|
+
*/
|
|
415
|
+
public async save(): Promise<undefined | BlockSaveResult> {
|
|
416
|
+
const extractedBlock = await this.extractToolData();
|
|
417
|
+
|
|
418
|
+
if (extractedBlock === undefined) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const tunesData: { [name: string]: BlockTuneData } = { ...this.unavailableTunesData };
|
|
423
|
+
|
|
424
|
+
[
|
|
425
|
+
...this.tunesInstances.entries(),
|
|
426
|
+
...this.defaultTunesInstances.entries(),
|
|
427
|
+
]
|
|
428
|
+
.forEach(([name, tune]) => {
|
|
429
|
+
if (isFunction(tune.save)) {
|
|
430
|
+
try {
|
|
431
|
+
tunesData[name] = tune.save();
|
|
432
|
+
} catch (e) {
|
|
433
|
+
log(`Tune ${tune.constructor.name} save method throws an Error %o`, 'warn', e);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Measuring execution time
|
|
440
|
+
*/
|
|
441
|
+
const measuringStart = window.performance.now();
|
|
442
|
+
|
|
443
|
+
this.lastSavedData = extractedBlock;
|
|
444
|
+
this.lastSavedTunes = { ...tunesData };
|
|
445
|
+
|
|
446
|
+
const measuringEnd = window.performance.now();
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
id: this.id,
|
|
450
|
+
tool: this.name,
|
|
451
|
+
data: extractedBlock,
|
|
452
|
+
tunes: tunesData,
|
|
453
|
+
time: measuringEnd - measuringStart,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Safely executes tool.save capturing possible errors without breaking the saver pipeline
|
|
459
|
+
*/
|
|
460
|
+
private async extractToolData(): Promise<BlockToolData | undefined> {
|
|
461
|
+
try {
|
|
462
|
+
const extracted = await this.toolInstance.save(this.pluginsContent as HTMLElement);
|
|
463
|
+
|
|
464
|
+
if (!this.isEmpty || extracted === undefined || extracted === null || typeof extracted !== 'object') {
|
|
465
|
+
return extracted;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const normalized = { ...extracted } as Record<string, unknown>;
|
|
469
|
+
const sanitizeField = (field: string): void => {
|
|
470
|
+
const value = normalized[field];
|
|
471
|
+
|
|
472
|
+
if (typeof value !== 'string') {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const container = document.createElement('div');
|
|
477
|
+
|
|
478
|
+
container.innerHTML = value;
|
|
479
|
+
|
|
480
|
+
if ($.isEmpty(container)) {
|
|
481
|
+
normalized[field] = '';
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
sanitizeField('text');
|
|
486
|
+
sanitizeField('html');
|
|
487
|
+
|
|
488
|
+
return normalized as BlockToolData;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
491
|
+
|
|
492
|
+
log(
|
|
493
|
+
`Saving process for ${this.name} tool failed due to the ${normalizedError}`,
|
|
494
|
+
'log',
|
|
495
|
+
normalizedError
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Uses Tool's validation method to check the correctness of output data
|
|
504
|
+
* Tool's validation method is optional
|
|
505
|
+
* @description Method returns true|false whether data passed the validation or not
|
|
506
|
+
* @param {BlockToolData} data - data to validate
|
|
507
|
+
* @returns {Promise<boolean>} valid
|
|
508
|
+
*/
|
|
509
|
+
public async validate(data: BlockToolData): Promise<boolean> {
|
|
510
|
+
if (this.toolInstance.validate instanceof Function) {
|
|
511
|
+
return await this.toolInstance.validate(data);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Returns data to render in Block Tunes menu.
|
|
519
|
+
* Splits block tunes into 2 groups: block specific tunes and common tunes
|
|
520
|
+
*/
|
|
521
|
+
public getTunes(): {
|
|
522
|
+
toolTunes: PopoverItemParams[];
|
|
523
|
+
commonTunes: PopoverItemParams[];
|
|
524
|
+
} {
|
|
525
|
+
const toolTunesPopoverParams: PopoverItemParams[] = [];
|
|
526
|
+
const commonTunesPopoverParams: PopoverItemParams[] = [];
|
|
527
|
+
const pushTuneConfig = (
|
|
528
|
+
tuneConfig: MenuConfigItem | MenuConfigItem[] | HTMLElement | undefined,
|
|
529
|
+
target: PopoverItemParams[]
|
|
530
|
+
): void => {
|
|
531
|
+
if (!tuneConfig) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if ($.isElement(tuneConfig)) {
|
|
536
|
+
target.push({
|
|
537
|
+
type: PopoverItemType.Html,
|
|
538
|
+
element: tuneConfig,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (Array.isArray(tuneConfig)) {
|
|
545
|
+
target.push(...tuneConfig);
|
|
546
|
+
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
target.push(tuneConfig);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
|
554
|
+
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
|
555
|
+
|
|
556
|
+
pushTuneConfig(tunesDefinedInTool, toolTunesPopoverParams);
|
|
557
|
+
|
|
558
|
+
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
|
559
|
+
const commonTunes = [
|
|
560
|
+
...this.tunesInstances.values(),
|
|
561
|
+
...this.defaultTunesInstances.values(),
|
|
562
|
+
].map(tuneInstance => tuneInstance.render());
|
|
563
|
+
|
|
564
|
+
/** Separate custom html from Popover items params for common tunes */
|
|
565
|
+
commonTunes.forEach(tuneConfig => {
|
|
566
|
+
pushTuneConfig(tuneConfig, commonTunesPopoverParams);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
toolTunes: toolTunesPopoverParams,
|
|
571
|
+
commonTunes: commonTunesPopoverParams,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Update current input index with selection anchor node
|
|
577
|
+
*/
|
|
578
|
+
public updateCurrentInput(): void {
|
|
579
|
+
/**
|
|
580
|
+
* If activeElement is native input, anchorNode points to its parent.
|
|
581
|
+
* So if it is native input use it instead of anchorNode
|
|
582
|
+
*
|
|
583
|
+
* If anchorNode is undefined, also use activeElement
|
|
584
|
+
*/
|
|
585
|
+
const anchorNode = SelectionUtils.anchorNode;
|
|
586
|
+
const activeElement = document.activeElement;
|
|
587
|
+
|
|
588
|
+
const resolveInput = (node: Node | null): HTMLElement | undefined => {
|
|
589
|
+
if (!node) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
594
|
+
|
|
595
|
+
if (element === null) {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const directMatch = this.inputs.find((input) => input === element || input.contains(element));
|
|
600
|
+
|
|
601
|
+
if (directMatch !== undefined) {
|
|
602
|
+
return directMatch;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const closestEditable = element.closest($.allInputsSelector);
|
|
606
|
+
|
|
607
|
+
if (!(closestEditable instanceof HTMLElement)) {
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const closestMatch = this.inputs.find((input) => input === closestEditable);
|
|
612
|
+
|
|
613
|
+
if (closestMatch !== undefined) {
|
|
614
|
+
return closestMatch;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return undefined;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
if ($.isNativeInput(activeElement)) {
|
|
621
|
+
this.currentInput = activeElement;
|
|
622
|
+
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const candidateInput = resolveInput(anchorNode) ?? (activeElement instanceof HTMLElement ? resolveInput(activeElement) : undefined);
|
|
627
|
+
|
|
628
|
+
if (candidateInput !== undefined) {
|
|
629
|
+
this.currentInput = candidateInput;
|
|
630
|
+
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (activeElement instanceof HTMLElement && this.inputs.includes(activeElement)) {
|
|
635
|
+
this.currentInput = activeElement;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Allows to say Blok that Block was changed. Used to manually trigger Blok's 'onChange' callback
|
|
641
|
+
* Can be useful for block changes invisible for blok core.
|
|
642
|
+
*/
|
|
643
|
+
public dispatchChange(): void {
|
|
644
|
+
this.didMutated();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Updates the block's data in-place without destroying the DOM element.
|
|
649
|
+
* This preserves focus and caret position during updates like undo/redo.
|
|
650
|
+
*
|
|
651
|
+
* @param newData - the new data to apply to the block
|
|
652
|
+
* @returns true if the update was performed in-place, false if a full re-render is needed
|
|
653
|
+
*/
|
|
654
|
+
public async setData(newData: BlockToolData): Promise<boolean> {
|
|
655
|
+
// Check if tool supports setData method
|
|
656
|
+
const toolSetData = (this.toolInstance as { setData?: (data: BlockToolData) => void | Promise<void> }).setData;
|
|
657
|
+
|
|
658
|
+
if (typeof toolSetData === 'function') {
|
|
659
|
+
try {
|
|
660
|
+
await toolSetData.call(this.toolInstance, newData);
|
|
661
|
+
this.lastSavedData = newData;
|
|
662
|
+
|
|
663
|
+
return true;
|
|
664
|
+
} catch (e) {
|
|
665
|
+
log(`Tool ${this.name} setData failed: ${e instanceof Error ? e.message : String(e)}`, 'warn');
|
|
666
|
+
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// For tools without setData, try to update innerHTML directly for simple text-based tools
|
|
672
|
+
const pluginsContent = this.toolRenderedElement;
|
|
673
|
+
|
|
674
|
+
if (!pluginsContent) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Handle simple text-based blocks (like paragraph) with a 'text' property
|
|
679
|
+
const hasTextProperty = 'text' in newData && typeof newData.text === 'string';
|
|
680
|
+
const isContentEditable = pluginsContent.getAttribute('contenteditable') === 'true';
|
|
681
|
+
|
|
682
|
+
if (hasTextProperty && isContentEditable) {
|
|
683
|
+
pluginsContent.innerHTML = newData.text as string;
|
|
684
|
+
this.lastSavedData = newData;
|
|
685
|
+
this.dropInputsCache();
|
|
686
|
+
this.toggleInputsEmptyMark();
|
|
687
|
+
this.call(BlockToolAPI.UPDATED);
|
|
688
|
+
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// For other tools, fall back to full re-render
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Call Tool instance destroy method
|
|
698
|
+
*/
|
|
699
|
+
public destroy(): void {
|
|
700
|
+
this.unwatchBlockMutations();
|
|
701
|
+
this.removeInputEvents();
|
|
702
|
+
|
|
703
|
+
/** Clean up drag and drop */
|
|
704
|
+
if (this.draggableCleanup) {
|
|
705
|
+
this.draggableCleanup();
|
|
706
|
+
this.draggableCleanup = null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
super.destroy();
|
|
710
|
+
|
|
711
|
+
if (isFunction(this.toolInstance.destroy)) {
|
|
712
|
+
this.toolInstance.destroy();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
|
|
718
|
+
* This method returns the entry that is related to the Block (depended on the Block data)
|
|
719
|
+
*/
|
|
720
|
+
public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {
|
|
721
|
+
const toolboxSettings = this.tool.toolbox;
|
|
722
|
+
|
|
723
|
+
if (!toolboxSettings) {
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* If Tool specifies just the single entry, treat it like an active
|
|
729
|
+
*/
|
|
730
|
+
if (toolboxSettings.length === 1) {
|
|
731
|
+
return Promise.resolve(toolboxSettings[0]);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* If we have several entries with their own data overrides,
|
|
736
|
+
* find those who matches some current data property
|
|
737
|
+
*
|
|
738
|
+
* Example:
|
|
739
|
+
* Tools' toolbox: [
|
|
740
|
+
* {title: "Heading 1", data: {level: 1} },
|
|
741
|
+
* {title: "Heading 2", data: {level: 2} }
|
|
742
|
+
* ]
|
|
743
|
+
*
|
|
744
|
+
* the Block data: {
|
|
745
|
+
* text: "Heading text",
|
|
746
|
+
* level: 2
|
|
747
|
+
* }
|
|
748
|
+
*
|
|
749
|
+
* that means that for the current block, the second toolbox item (matched by "{level: 2}") is active
|
|
750
|
+
*/
|
|
751
|
+
const blockData = await this.data;
|
|
752
|
+
|
|
753
|
+
return toolboxSettings.find((item) => {
|
|
754
|
+
return isSameBlockData(item.data, blockData);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Exports Block data as string using conversion config
|
|
760
|
+
*/
|
|
761
|
+
public async exportDataAsString(): Promise<string> {
|
|
762
|
+
const blockData = await this.data;
|
|
763
|
+
|
|
764
|
+
return convertBlockDataToString(blockData, this.tool.conversionConfig);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Link to blok dom change callback. Used to remove listener on remove
|
|
769
|
+
*/
|
|
770
|
+
private redactorDomChangedCallback: (payload: RedactorDomChangedPayload) => void = () => {};
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Find and return all editable elements (contenteditable and native inputs) in the Tool HTML
|
|
774
|
+
*/
|
|
775
|
+
public get inputs(): HTMLElement[] {
|
|
776
|
+
/**
|
|
777
|
+
* Return from cache if existed
|
|
778
|
+
*/
|
|
779
|
+
if (this.cachedInputs.length !== 0) {
|
|
780
|
+
return this.cachedInputs;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const inputs = $.findAllInputs(this.holder);
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* If inputs amount was changed we need to check if input index is bigger then inputs array length
|
|
787
|
+
*/
|
|
788
|
+
if (this.inputIndex > inputs.length - 1) {
|
|
789
|
+
this.inputIndex = inputs.length - 1;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Cache inputs
|
|
794
|
+
*/
|
|
795
|
+
this.cachedInputs = inputs;
|
|
796
|
+
|
|
797
|
+
return inputs;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Return current Tool`s input
|
|
802
|
+
* If Block doesn't contain inputs, return undefined
|
|
803
|
+
*/
|
|
804
|
+
public get currentInput(): HTMLElement | undefined {
|
|
805
|
+
return this.inputs[this.inputIndex];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Set input index to the passed element
|
|
810
|
+
* @param element - HTML Element to set as current input
|
|
811
|
+
*/
|
|
812
|
+
public set currentInput(element: HTMLElement | undefined) {
|
|
813
|
+
if (element === undefined) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const index = this.inputs.findIndex((input) => input === element || input.contains(element));
|
|
818
|
+
|
|
819
|
+
if (index !== -1) {
|
|
820
|
+
this.inputIndex = index;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Return first Tool`s input
|
|
826
|
+
* If Block doesn't contain inputs, return undefined
|
|
827
|
+
*/
|
|
828
|
+
public get firstInput(): HTMLElement | undefined {
|
|
829
|
+
return this.inputs[0];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Return first Tool`s input
|
|
834
|
+
* If Block doesn't contain inputs, return undefined
|
|
835
|
+
*/
|
|
836
|
+
public get lastInput(): HTMLElement | undefined {
|
|
837
|
+
const inputs = this.inputs;
|
|
838
|
+
|
|
839
|
+
return inputs[inputs.length - 1];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Return next Tool`s input or undefined if it doesn't exist
|
|
844
|
+
* If Block doesn't contain inputs, return undefined
|
|
845
|
+
*/
|
|
846
|
+
public get nextInput(): HTMLElement | undefined {
|
|
847
|
+
return this.inputs[this.inputIndex + 1];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Return previous Tool`s input or undefined if it doesn't exist
|
|
852
|
+
* If Block doesn't contain inputs, return undefined
|
|
853
|
+
*/
|
|
854
|
+
public get previousInput(): HTMLElement | undefined {
|
|
855
|
+
return this.inputs[this.inputIndex - 1];
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Get Block's JSON data
|
|
860
|
+
* @returns {object}
|
|
861
|
+
*/
|
|
862
|
+
public get data(): Promise<BlockToolData> {
|
|
863
|
+
return this.save().then((savedObject) => {
|
|
864
|
+
if (savedObject && !isEmpty(savedObject.data)) {
|
|
865
|
+
return savedObject.data;
|
|
866
|
+
} else {
|
|
867
|
+
return {};
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Returns last successfully extracted block data
|
|
874
|
+
*/
|
|
875
|
+
public get preservedData(): BlockToolData {
|
|
876
|
+
return this.lastSavedData ?? {};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Returns last successfully extracted tune data
|
|
881
|
+
*/
|
|
882
|
+
public get preservedTunes(): { [name: string]: BlockTuneData } {
|
|
883
|
+
return this.lastSavedTunes ?? {};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Returns tool's sanitizer config
|
|
888
|
+
* @returns {object}
|
|
889
|
+
*/
|
|
890
|
+
public get sanitize(): SanitizerConfig {
|
|
891
|
+
return this.tool.sanitizeConfig;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* is block mergeable
|
|
896
|
+
* We plugin have merge function then we call it mergeable
|
|
897
|
+
* @returns {boolean}
|
|
898
|
+
*/
|
|
899
|
+
public get mergeable(): boolean {
|
|
900
|
+
return isFunction(this.toolInstance.merge);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* If Block contains inputs, it is focusable
|
|
905
|
+
*/
|
|
906
|
+
public get focusable(): boolean {
|
|
907
|
+
return this.inputs.length !== 0;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Check block for emptiness
|
|
912
|
+
* @returns {boolean}
|
|
913
|
+
*/
|
|
914
|
+
public get isEmpty(): boolean {
|
|
915
|
+
const emptyText = $.isEmpty(this.pluginsContent, '/');
|
|
916
|
+
const emptyMedia = !this.hasMedia;
|
|
917
|
+
|
|
918
|
+
return emptyText && emptyMedia;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Check if block has a media content such as images, iframe and other
|
|
923
|
+
* @returns {boolean}
|
|
924
|
+
*/
|
|
925
|
+
public get hasMedia(): boolean {
|
|
926
|
+
/**
|
|
927
|
+
* This tags represents media-content
|
|
928
|
+
* @type {string[]}
|
|
929
|
+
*/
|
|
930
|
+
const mediaTags = [
|
|
931
|
+
'img',
|
|
932
|
+
'iframe',
|
|
933
|
+
'video',
|
|
934
|
+
'audio',
|
|
935
|
+
'source',
|
|
936
|
+
'input',
|
|
937
|
+
'textarea',
|
|
938
|
+
'twitterwidget',
|
|
939
|
+
];
|
|
940
|
+
|
|
941
|
+
return !!this.holder.querySelector(mediaTags.join(','));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Set selected state
|
|
946
|
+
* We don't need to mark Block as Selected when it is empty
|
|
947
|
+
* @param {boolean} state - 'true' to select, 'false' to remove selection
|
|
948
|
+
*/
|
|
949
|
+
public set selected(state: boolean) {
|
|
950
|
+
if (state) {
|
|
951
|
+
this.holder.setAttribute(DATA_ATTR.selected, 'true');
|
|
952
|
+
} else {
|
|
953
|
+
this.holder.removeAttribute(DATA_ATTR.selected);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (this.contentElement) {
|
|
957
|
+
const stretchedClass = this.stretched ? Block.styles.contentStretched : '';
|
|
958
|
+
|
|
959
|
+
this.contentElement.className = state
|
|
960
|
+
? twMerge(Block.styles.content, Block.styles.contentSelected)
|
|
961
|
+
: twMerge(Block.styles.content, stretchedClass);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const fakeCursorWillBeAdded = state === true && SelectionUtils.isRangeInsideContainer(this.holder);
|
|
965
|
+
const fakeCursorWillBeRemoved = state === false && SelectionUtils.isFakeCursorInsideContainer(this.holder);
|
|
966
|
+
|
|
967
|
+
if (!fakeCursorWillBeAdded && !fakeCursorWillBeRemoved) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
this.blokEventBus?.emit(FakeCursorAboutToBeToggled, { state }); // mutex
|
|
972
|
+
|
|
973
|
+
if (fakeCursorWillBeAdded) {
|
|
974
|
+
SelectionUtils.addFakeCursor();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (fakeCursorWillBeRemoved) {
|
|
978
|
+
SelectionUtils.removeFakeCursor(this.holder);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.blokEventBus?.emit(FakeCursorHaveBeenSet, { state });
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Returns True if it is Selected
|
|
986
|
+
* @returns {boolean}
|
|
987
|
+
*/
|
|
988
|
+
public get selected(): boolean {
|
|
989
|
+
return this.holder.getAttribute(DATA_ATTR.selected) === 'true';
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Set stretched state
|
|
994
|
+
* @param {boolean} state - 'true' to enable, 'false' to disable stretched state
|
|
995
|
+
*/
|
|
996
|
+
public setStretchState(state: boolean): void {
|
|
997
|
+
if (state) {
|
|
998
|
+
this.holder.setAttribute(DATA_ATTR.stretched, 'true');
|
|
999
|
+
} else {
|
|
1000
|
+
this.holder.removeAttribute(DATA_ATTR.stretched);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (this.contentElement && !this.selected) {
|
|
1004
|
+
this.contentElement.className = state
|
|
1005
|
+
? twMerge(Block.styles.content, Block.styles.contentStretched)
|
|
1006
|
+
: Block.styles.content;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Backward-compatible setter for stretched state
|
|
1012
|
+
* @param state - true to enable, false to disable stretched state
|
|
1013
|
+
*/
|
|
1014
|
+
public set stretched(state: boolean) {
|
|
1015
|
+
this.setStretchState(state);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Return Block's stretched state
|
|
1020
|
+
* @returns {boolean}
|
|
1021
|
+
*/
|
|
1022
|
+
public get stretched(): boolean {
|
|
1023
|
+
return this.holder.getAttribute(DATA_ATTR.stretched) === 'true';
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Returns Plugins content
|
|
1029
|
+
* @returns {HTMLElement}
|
|
1030
|
+
*/
|
|
1031
|
+
public get pluginsContent(): HTMLElement {
|
|
1032
|
+
if (this.toolRenderedElement === null) {
|
|
1033
|
+
throw new Error('Block pluginsContent is not yet initialized');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return this.toolRenderedElement;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Make default Block wrappers and put Tool`s content there
|
|
1041
|
+
* @returns {HTMLDivElement}
|
|
1042
|
+
*/
|
|
1043
|
+
private compose(): HTMLDivElement {
|
|
1044
|
+
const wrapper = $.make('div', Block.styles.wrapper) as HTMLDivElement;
|
|
1045
|
+
const contentNode = $.make('div', Block.styles.content);
|
|
1046
|
+
|
|
1047
|
+
this.contentElement = contentNode;
|
|
1048
|
+
|
|
1049
|
+
// Set data attributes for block element and content
|
|
1050
|
+
wrapper.setAttribute(DATA_ATTR.element, '');
|
|
1051
|
+
contentNode.setAttribute(DATA_ATTR.elementContent, '');
|
|
1052
|
+
contentNode.setAttribute('data-blok-testid', 'block-content');
|
|
1053
|
+
const pluginsContent = this.toolInstance.render();
|
|
1054
|
+
|
|
1055
|
+
wrapper.setAttribute('data-blok-testid', 'block-wrapper');
|
|
1056
|
+
|
|
1057
|
+
if (this.name && !wrapper.hasAttribute('data-blok-component')) {
|
|
1058
|
+
wrapper.setAttribute('data-blok-component', this.name);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Export id to the DOM three
|
|
1063
|
+
* Useful for standalone modules development. For example, allows to identify Block by some child node. Or scroll to a particular Block by id.
|
|
1064
|
+
*/
|
|
1065
|
+
wrapper.setAttribute('data-blok-id', this.id);
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Saving a reference to plugin's content element for guaranteed accessing it later
|
|
1069
|
+
* Handle both synchronous HTMLElement and Promise<HTMLElement> cases
|
|
1070
|
+
*/
|
|
1071
|
+
if (pluginsContent instanceof Promise) {
|
|
1072
|
+
// Handle async render: resolve the promise and update DOM when ready
|
|
1073
|
+
pluginsContent.then((resolvedElement) => {
|
|
1074
|
+
this.toolRenderedElement = resolvedElement;
|
|
1075
|
+
this.addToolDataAttributes(resolvedElement, wrapper);
|
|
1076
|
+
contentNode.appendChild(resolvedElement);
|
|
1077
|
+
this.readyResolver?.();
|
|
1078
|
+
}).catch((error) => {
|
|
1079
|
+
log(`Tool render promise rejected: %o`, 'error', error);
|
|
1080
|
+
this.readyResolver?.();
|
|
1081
|
+
});
|
|
1082
|
+
} else {
|
|
1083
|
+
// Handle synchronous render
|
|
1084
|
+
this.toolRenderedElement = pluginsContent;
|
|
1085
|
+
this.addToolDataAttributes(pluginsContent, wrapper);
|
|
1086
|
+
contentNode.appendChild(pluginsContent);
|
|
1087
|
+
this.readyResolver?.();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Block Tunes might wrap Block's content node to provide any UI changes
|
|
1092
|
+
*
|
|
1093
|
+
* <tune2wrapper>
|
|
1094
|
+
* <tune1wrapper>
|
|
1095
|
+
* <blockContent />
|
|
1096
|
+
* </tune1wrapper>
|
|
1097
|
+
* </tune2wrapper>
|
|
1098
|
+
*/
|
|
1099
|
+
const wrappedContentNode: HTMLElement = [...this.tunesInstances.values(), ...this.defaultTunesInstances.values()]
|
|
1100
|
+
.reduce((acc, tune) => {
|
|
1101
|
+
if (isFunction(tune.wrap)) {
|
|
1102
|
+
try {
|
|
1103
|
+
return tune.wrap(acc);
|
|
1104
|
+
} catch (e) {
|
|
1105
|
+
log(`Tune ${tune.constructor.name} wrap method throws an Error %o`, 'warn', e);
|
|
1106
|
+
|
|
1107
|
+
return acc;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return acc;
|
|
1112
|
+
}, contentNode);
|
|
1113
|
+
|
|
1114
|
+
wrapper.appendChild(wrappedContentNode);
|
|
1115
|
+
|
|
1116
|
+
return wrapper;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Add data attributes to tool-rendered element based on tool name
|
|
1121
|
+
* @param element - The tool-rendered element
|
|
1122
|
+
* @param blockWrapper - Block wrapper that hosts the tool render
|
|
1123
|
+
* @private
|
|
1124
|
+
*/
|
|
1125
|
+
private addToolDataAttributes(element: HTMLElement, blockWrapper: HTMLDivElement): void {
|
|
1126
|
+
/**
|
|
1127
|
+
* Add data-blok-component attribute to identify the tool type used for the block.
|
|
1128
|
+
* Some tools (like Paragraph) add their own class names, but we can rely on the tool name for all cases.
|
|
1129
|
+
*/
|
|
1130
|
+
if (this.name && !blockWrapper.hasAttribute('data-blok-component')) {
|
|
1131
|
+
blockWrapper.setAttribute('data-blok-component', this.name);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const placeholderAttribute = 'data-blok-placeholder';
|
|
1135
|
+
const placeholder = this.config?.placeholder;
|
|
1136
|
+
const placeholderText = typeof placeholder === 'string' ? placeholder.trim() : '';
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Paragraph tool handles its own placeholder via data-blok-placeholder-active attribute
|
|
1140
|
+
* with focus-only classes, so we skip the block-level placeholder for it.
|
|
1141
|
+
*/
|
|
1142
|
+
if (this.name === 'paragraph') {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Placeholder styling classes using Tailwind arbitrary variants.
|
|
1148
|
+
* Applied to ::before pseudo-element only when element is empty.
|
|
1149
|
+
* Uses arbitrary properties for `content: attr(data-blok-placeholder)`.
|
|
1150
|
+
*/
|
|
1151
|
+
const placeholderClasses = [
|
|
1152
|
+
'empty:before:pointer-events-none',
|
|
1153
|
+
'empty:before:text-gray-text',
|
|
1154
|
+
'empty:before:cursor-text',
|
|
1155
|
+
'empty:before:content-[attr(data-blok-placeholder)]',
|
|
1156
|
+
'[&[data-blok-empty=true]]:before:pointer-events-none',
|
|
1157
|
+
'[&[data-blok-empty=true]]:before:text-gray-text',
|
|
1158
|
+
'[&[data-blok-empty=true]]:before:cursor-text',
|
|
1159
|
+
'[&[data-blok-empty=true]]:before:content-[attr(data-blok-placeholder)]',
|
|
1160
|
+
];
|
|
1161
|
+
|
|
1162
|
+
if (placeholderText.length > 0) {
|
|
1163
|
+
element.setAttribute(placeholderAttribute, placeholderText);
|
|
1164
|
+
element.classList.add(...placeholderClasses);
|
|
1165
|
+
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (placeholder === false && element.hasAttribute(placeholderAttribute)) {
|
|
1170
|
+
element.removeAttribute(placeholderAttribute);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Instantiate Block Tunes
|
|
1176
|
+
* @param tunesData - current Block tunes data
|
|
1177
|
+
* @private
|
|
1178
|
+
*/
|
|
1179
|
+
private composeTunes(tunesData: { [name: string]: BlockTuneData }): void {
|
|
1180
|
+
Array.from(this.tunes.values()).forEach((tune) => {
|
|
1181
|
+
const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances;
|
|
1182
|
+
|
|
1183
|
+
collection.set(tune.name, tune.create(tunesData[tune.name], this.blockAPI));
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Check if there is some data for not available tunes
|
|
1188
|
+
*/
|
|
1189
|
+
Object.entries(tunesData).forEach(([name, data]) => {
|
|
1190
|
+
if (!this.tunesInstances.has(name)) {
|
|
1191
|
+
this.unavailableTunesData[name] = data;
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Is fired when text input or contentEditable is focused
|
|
1198
|
+
*/
|
|
1199
|
+
private handleFocus = (): void => {
|
|
1200
|
+
/**
|
|
1201
|
+
* Drop inputs cache to query the new ones
|
|
1202
|
+
*/
|
|
1203
|
+
this.dropInputsCache();
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Update current input
|
|
1207
|
+
*/
|
|
1208
|
+
this.updateCurrentInput();
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Adds focus event listeners to all inputs and contenteditable
|
|
1213
|
+
*/
|
|
1214
|
+
private addInputEvents(): void {
|
|
1215
|
+
this.inputs.forEach(input => {
|
|
1216
|
+
input.addEventListener('focus', this.handleFocus);
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* If input is native input add oninput listener to observe changes
|
|
1220
|
+
*/
|
|
1221
|
+
if ($.isNativeInput(input)) {
|
|
1222
|
+
input.addEventListener('input', this.didMutated as EventListener);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* removes focus event listeners from all inputs and contenteditable
|
|
1229
|
+
*/
|
|
1230
|
+
private removeInputEvents(): void {
|
|
1231
|
+
this.inputs.forEach(input => {
|
|
1232
|
+
input.removeEventListener('focus', this.handleFocus);
|
|
1233
|
+
|
|
1234
|
+
if ($.isNativeInput(input)) {
|
|
1235
|
+
input.removeEventListener('input', this.didMutated as EventListener);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Is fired when DOM mutation has been happened
|
|
1242
|
+
* @param mutationsOrInputEvent - actual changes
|
|
1243
|
+
* - MutationRecord[] - any DOM change
|
|
1244
|
+
* - InputEvent — <input> change
|
|
1245
|
+
* - undefined — manual triggering of block.dispatchChange()
|
|
1246
|
+
*/
|
|
1247
|
+
private readonly didMutated = (mutationsOrInputEvent: MutationRecord[] | InputEvent | undefined = undefined): void => {
|
|
1248
|
+
/**
|
|
1249
|
+
* Block API have dispatchChange() method. In this case, mutations list will be undefined.
|
|
1250
|
+
*/
|
|
1251
|
+
const isManuallyDispatched = mutationsOrInputEvent === undefined;
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* True if didMutated has been called as "input" event handler
|
|
1255
|
+
*/
|
|
1256
|
+
const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent;
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* If tool updates its own root element, we need to renew it in our memory
|
|
1260
|
+
*/
|
|
1261
|
+
if (!isManuallyDispatched && !isInputEventHandler) {
|
|
1262
|
+
this.detectToolRootChange(mutationsOrInputEvent);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* We won't fire a Block mutation event if mutation contain only nodes marked with 'data-blok-mutation-free' attributes
|
|
1267
|
+
*/
|
|
1268
|
+
const shouldFireUpdate = (() => {
|
|
1269
|
+
if (isManuallyDispatched || isInputEventHandler) {
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Update from 2023, Feb 17:
|
|
1275
|
+
* Changed mutationsOrInputEvent.some() to mutationsOrInputEvent.every()
|
|
1276
|
+
* since there could be a real mutations same-time with mutation-free changes,
|
|
1277
|
+
* for example when Block Tune change: block is changing along with FakeCursor (mutation-free) removing
|
|
1278
|
+
* — we should fire 'didMutated' event in that case
|
|
1279
|
+
*/
|
|
1280
|
+
const everyRecordIsMutationFree = mutationsOrInputEvent.length > 0 && mutationsOrInputEvent.every((record) => {
|
|
1281
|
+
const { addedNodes, removedNodes, target } = record;
|
|
1282
|
+
const changedNodes = [
|
|
1283
|
+
...Array.from(addedNodes),
|
|
1284
|
+
...Array.from(removedNodes),
|
|
1285
|
+
target,
|
|
1286
|
+
];
|
|
1287
|
+
|
|
1288
|
+
return changedNodes.every((node) => {
|
|
1289
|
+
const elementToCheck: Element | null = !$.isElement(node)
|
|
1290
|
+
? node.parentElement ?? null
|
|
1291
|
+
: node;
|
|
1292
|
+
|
|
1293
|
+
if (elementToCheck === null) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return elementToCheck.closest('[data-blok-mutation-free="true"]') !== null;
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
return !everyRecordIsMutationFree;
|
|
1302
|
+
})();
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* In case some mutation free elements are added or removed, do not trigger didMutated event
|
|
1306
|
+
*/
|
|
1307
|
+
if (!shouldFireUpdate) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
this.dropInputsCache();
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Update current input
|
|
1315
|
+
*/
|
|
1316
|
+
this.updateCurrentInput();
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* We mark inputs with 'data-blok-empty' attribute, so new inputs should be marked as well
|
|
1320
|
+
*/
|
|
1321
|
+
this.toggleInputsEmptyMark();
|
|
1322
|
+
|
|
1323
|
+
this.call(BlockToolAPI.UPDATED);
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Emit a Block Event with current Block instance.
|
|
1327
|
+
* Block Manager subscribed to these events
|
|
1328
|
+
*/
|
|
1329
|
+
this.emit('didMutated', this);
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Listen common blok Dom Changed event and detect mutations related to the Block
|
|
1334
|
+
*/
|
|
1335
|
+
private watchBlockMutations(): void {
|
|
1336
|
+
/**
|
|
1337
|
+
* Save callback to a property to remove it on Block destroy
|
|
1338
|
+
* @param payload - event payload
|
|
1339
|
+
*/
|
|
1340
|
+
this.redactorDomChangedCallback = (payload) => {
|
|
1341
|
+
const { mutations } = payload;
|
|
1342
|
+
|
|
1343
|
+
const toolElement = this.toolRenderedElement;
|
|
1344
|
+
|
|
1345
|
+
if (toolElement === null) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Filter mutations to only include those that belong to this block.
|
|
1351
|
+
* Previously, all mutations were passed when any belonged to the block,
|
|
1352
|
+
* which could include mutations from other parts of the blok.
|
|
1353
|
+
*/
|
|
1354
|
+
const blockMutations = mutations.filter(record => isMutationBelongsToElement(record, toolElement));
|
|
1355
|
+
|
|
1356
|
+
if (blockMutations.length > 0) {
|
|
1357
|
+
this.didMutated(blockMutations);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
this.blokEventBus?.on(RedactorDomChanged, this.redactorDomChangedCallback);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Remove redactor dom change event listener.
|
|
1366
|
+
* Can be called to stop watching mutations before destroying the block.
|
|
1367
|
+
*/
|
|
1368
|
+
public unwatchBlockMutations(): void {
|
|
1369
|
+
this.blokEventBus?.off(RedactorDomChanged, this.redactorDomChangedCallback);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Refreshes the reference to the tool's root element by inspecting the block content.
|
|
1374
|
+
* Call this after operations (like onPaste) that might cause the tool to replace its element,
|
|
1375
|
+
* especially when mutation observers haven't been set up yet.
|
|
1376
|
+
*/
|
|
1377
|
+
public refreshToolRootElement(): void {
|
|
1378
|
+
const contentNode = this.holder.querySelector(createSelector(DATA_ATTR.elementContent));
|
|
1379
|
+
|
|
1380
|
+
if (!contentNode) {
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const firstChild = contentNode.firstElementChild as HTMLElement | null;
|
|
1385
|
+
|
|
1386
|
+
if (firstChild && firstChild !== this.toolRenderedElement) {
|
|
1387
|
+
this.toolRenderedElement = firstChild;
|
|
1388
|
+
this.dropInputsCache();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Sometimes Tool can replace own main element, for example H2 -> H4 or UL -> OL
|
|
1394
|
+
* We need to detect such changes and update a link to tools main element with the new one
|
|
1395
|
+
* @param mutations - records of block content mutations
|
|
1396
|
+
*/
|
|
1397
|
+
private detectToolRootChange(mutations: MutationRecord[]): void {
|
|
1398
|
+
const toolElement = this.toolRenderedElement;
|
|
1399
|
+
|
|
1400
|
+
if (toolElement === null) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
mutations.forEach(record => {
|
|
1405
|
+
const toolRootHasBeenUpdated = Array.from(record.removedNodes).includes(toolElement);
|
|
1406
|
+
|
|
1407
|
+
if (toolRootHasBeenUpdated) {
|
|
1408
|
+
const newToolElement = record.addedNodes[record.addedNodes.length - 1];
|
|
1409
|
+
|
|
1410
|
+
this.toolRenderedElement = newToolElement as HTMLElement;
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Clears inputs cached value
|
|
1417
|
+
*/
|
|
1418
|
+
private dropInputsCache(): void {
|
|
1419
|
+
this.cachedInputs = [];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Mark inputs with 'data-blok-empty' attribute with the empty state
|
|
1424
|
+
*/
|
|
1425
|
+
private toggleInputsEmptyMark(): void {
|
|
1426
|
+
this.inputs.forEach(toggleEmptyMark);
|
|
1427
|
+
}
|
|
1428
|
+
}
|