@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,1098 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class History
|
|
3
|
+
* @classdesc Manages undo/redo functionality using state snapshots
|
|
4
|
+
* @module History
|
|
5
|
+
*/
|
|
6
|
+
import { Module } from '../__module';
|
|
7
|
+
import type { OutputData, OutputBlockData } from '../../../types';
|
|
8
|
+
import { BlockChanged, HistoryStateChanged } from '../events';
|
|
9
|
+
import type { BlockMutationEvent } from '../../../types/events/block';
|
|
10
|
+
import { Shortcuts } from '../utils/shortcuts';
|
|
11
|
+
import type { Block } from '../block';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default maximum history stack size
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_MAX_HISTORY_LENGTH = 30;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default debounce time for content changes (ms)
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_DEBOUNCE_TIME = 200;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Time to wait after restore before accepting new changes (ms)
|
|
25
|
+
* This prevents late-firing events from corrupting history
|
|
26
|
+
*/
|
|
27
|
+
const RESTORE_COOLDOWN_TIME = 100;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Represents caret position for restoration after undo/redo
|
|
31
|
+
*/
|
|
32
|
+
interface CaretPosition {
|
|
33
|
+
/**
|
|
34
|
+
* ID of the block containing the caret
|
|
35
|
+
*/
|
|
36
|
+
blockId: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Index of the block in the editor at capture time.
|
|
40
|
+
* Used as fallback when blockId lookup fails (e.g., block was deleted).
|
|
41
|
+
*/
|
|
42
|
+
blockIndex: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Index of the input element within the block
|
|
46
|
+
*/
|
|
47
|
+
inputIndex: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Character offset within the input
|
|
51
|
+
*/
|
|
52
|
+
offset: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* History entry representing a document state
|
|
57
|
+
*/
|
|
58
|
+
interface HistoryEntry {
|
|
59
|
+
/**
|
|
60
|
+
* The document state snapshot
|
|
61
|
+
*/
|
|
62
|
+
state: OutputData;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Timestamp when this entry was created
|
|
66
|
+
*/
|
|
67
|
+
timestamp: number;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Caret position at the time of the snapshot
|
|
71
|
+
*/
|
|
72
|
+
caretPosition?: CaretPosition;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* History module for undo/redo functionality
|
|
77
|
+
*
|
|
78
|
+
* Uses state snapshots approach:
|
|
79
|
+
* - Captures full document state after mutations
|
|
80
|
+
* - Debounces rapid changes (typing) into single undo steps
|
|
81
|
+
* - Provides keyboard shortcuts (Cmd+Z / Cmd+Shift+Z)
|
|
82
|
+
*/
|
|
83
|
+
export class History extends Module {
|
|
84
|
+
/**
|
|
85
|
+
* Tracks which History instance should respond to global shortcuts.
|
|
86
|
+
* Set to the instance that last received a block mutation.
|
|
87
|
+
*/
|
|
88
|
+
private static activeInstance: History | null = null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Stack of past states for undo
|
|
92
|
+
*/
|
|
93
|
+
private undoStack: HistoryEntry[] = [];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stack of future states for redo
|
|
97
|
+
*/
|
|
98
|
+
private redoStack: HistoryEntry[] = [];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shortcut names registered on document for cleanup
|
|
102
|
+
*/
|
|
103
|
+
private registeredShortcuts: Array<{ name: string; element: HTMLElement | Document }> = [];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Debounce timeout for batching rapid changes
|
|
107
|
+
*/
|
|
108
|
+
private debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Flag to prevent recording during undo/redo operations
|
|
112
|
+
*/
|
|
113
|
+
private isPerformingUndoRedo = false;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Flag indicating whether initial state has been captured
|
|
117
|
+
*/
|
|
118
|
+
private initialStateCaptured = false;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Maximum number of entries in history stack
|
|
122
|
+
*/
|
|
123
|
+
private get maxHistoryLength(): number {
|
|
124
|
+
return (this.config as { maxHistoryLength?: number }).maxHistoryLength ?? DEFAULT_MAX_HISTORY_LENGTH;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Debounce time for batching changes
|
|
129
|
+
*/
|
|
130
|
+
private get debounceTime(): number {
|
|
131
|
+
return (this.config as { historyDebounceTime?: number }).historyDebounceTime ?? DEFAULT_DEBOUNCE_TIME;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether to use document-level shortcuts for undo/redo
|
|
136
|
+
*/
|
|
137
|
+
private get globalUndoRedo(): boolean {
|
|
138
|
+
return (this.config as { globalUndoRedo?: boolean }).globalUndoRedo ?? true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Module preparation
|
|
143
|
+
* Sets up event listeners and keyboard shortcuts
|
|
144
|
+
*/
|
|
145
|
+
public async prepare(): Promise<void> {
|
|
146
|
+
this.setupEventListeners();
|
|
147
|
+
this.setupKeyboardShortcuts();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Captures the initial document state
|
|
152
|
+
* Should be called after rendering is complete
|
|
153
|
+
*/
|
|
154
|
+
public async captureInitialState(): Promise<void> {
|
|
155
|
+
if (this.initialStateCaptured) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const state = await this.getCurrentState();
|
|
160
|
+
|
|
161
|
+
if (state) {
|
|
162
|
+
const caretPosition = this.getCaretPosition();
|
|
163
|
+
|
|
164
|
+
this.undoStack = [{
|
|
165
|
+
state,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
caretPosition,
|
|
168
|
+
}];
|
|
169
|
+
this.initialStateCaptured = true;
|
|
170
|
+
this.emitStateChanged();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Performs undo operation
|
|
176
|
+
* @returns true if undo was performed, false if nothing to undo
|
|
177
|
+
*/
|
|
178
|
+
public async undo(): Promise<boolean> {
|
|
179
|
+
// Need at least 2 entries: current state + previous state to restore
|
|
180
|
+
if (this.undoStack.length < 2) {
|
|
181
|
+
// Preserve caret position when there's nothing to undo
|
|
182
|
+
this.preserveCaretPosition();
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.isPerformingUndoRedo) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.isPerformingUndoRedo = true;
|
|
192
|
+
this.clearDebounce();
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Pop current state and push to redo stack
|
|
196
|
+
const currentEntry = this.undoStack.pop();
|
|
197
|
+
|
|
198
|
+
if (currentEntry) {
|
|
199
|
+
this.redoStack.push(currentEntry);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get previous state to restore
|
|
203
|
+
const previousEntry = this.undoStack[this.undoStack.length - 1];
|
|
204
|
+
|
|
205
|
+
if (previousEntry) {
|
|
206
|
+
// Pass both target caret position and fallback from current state
|
|
207
|
+
// When a block is deleted, we want to fall back to the block preceding the deleted block
|
|
208
|
+
const fallbackIndex = currentEntry?.caretPosition
|
|
209
|
+
? Math.max(0, currentEntry.caretPosition.blockIndex - 1)
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
// If the previous entry has no caret position (e.g., initial state),
|
|
213
|
+
// use the current entry's caret position as a fallback
|
|
214
|
+
const fallbackCaretPosition = !previousEntry.caretPosition
|
|
215
|
+
? currentEntry?.caretPosition
|
|
216
|
+
: undefined;
|
|
217
|
+
|
|
218
|
+
await this.restoreState(
|
|
219
|
+
previousEntry.state,
|
|
220
|
+
previousEntry.caretPosition,
|
|
221
|
+
fallbackIndex,
|
|
222
|
+
fallbackCaretPosition
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Clean up any orphaned fake background elements that may have been restored
|
|
226
|
+
this.Blok.SelectionAPI.methods.clearFakeBackground();
|
|
227
|
+
|
|
228
|
+
this.emitStateChanged();
|
|
229
|
+
|
|
230
|
+
// Keep the flag true for a short period to ignore late events
|
|
231
|
+
await this.cooldown();
|
|
232
|
+
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
} finally {
|
|
238
|
+
this.isPerformingUndoRedo = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Performs redo operation
|
|
244
|
+
* @returns true if redo was performed, false if nothing to redo
|
|
245
|
+
*/
|
|
246
|
+
public async redo(): Promise<boolean> {
|
|
247
|
+
if (this.redoStack.length === 0) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.isPerformingUndoRedo) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.isPerformingUndoRedo = true;
|
|
256
|
+
this.clearDebounce();
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// Get the current state before popping (to use its caret as fallback)
|
|
260
|
+
const currentEntry = this.undoStack[this.undoStack.length - 1];
|
|
261
|
+
const entryToRestore = this.redoStack.pop();
|
|
262
|
+
|
|
263
|
+
if (entryToRestore) {
|
|
264
|
+
this.undoStack.push(entryToRestore);
|
|
265
|
+
|
|
266
|
+
// Pass both target caret position and fallback from current state
|
|
267
|
+
// When a block is deleted during redo, fall back to the block preceding the deleted block
|
|
268
|
+
const fallbackIndex = currentEntry?.caretPosition
|
|
269
|
+
? Math.max(0, currentEntry.caretPosition.blockIndex - 1)
|
|
270
|
+
: undefined;
|
|
271
|
+
|
|
272
|
+
await this.restoreState(entryToRestore.state, entryToRestore.caretPosition, fallbackIndex);
|
|
273
|
+
|
|
274
|
+
// Clean up any orphaned fake background elements that may have been restored
|
|
275
|
+
this.Blok.SelectionAPI.methods.clearFakeBackground();
|
|
276
|
+
|
|
277
|
+
this.emitStateChanged();
|
|
278
|
+
|
|
279
|
+
// Keep the flag true for a short period to ignore late events
|
|
280
|
+
await this.cooldown();
|
|
281
|
+
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return false;
|
|
286
|
+
} finally {
|
|
287
|
+
this.isPerformingUndoRedo = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Returns whether undo is available
|
|
293
|
+
*/
|
|
294
|
+
public canUndo(): boolean {
|
|
295
|
+
return this.undoStack.length > 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Returns whether redo is available
|
|
300
|
+
*/
|
|
301
|
+
public canRedo(): boolean {
|
|
302
|
+
return this.redoStack.length > 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Clears history stacks
|
|
307
|
+
*/
|
|
308
|
+
public clear(): void {
|
|
309
|
+
this.clearDebounce();
|
|
310
|
+
this.undoStack = [];
|
|
311
|
+
this.redoStack = [];
|
|
312
|
+
this.initialStateCaptured = false;
|
|
313
|
+
this.emitStateChanged();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sets up listeners for block mutation events
|
|
318
|
+
*/
|
|
319
|
+
private setupEventListeners(): void {
|
|
320
|
+
this.eventsDispatcher.on(BlockChanged, (payload) => {
|
|
321
|
+
this.handleBlockMutation(payload.event);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Sets up keyboard shortcuts for undo/redo
|
|
327
|
+
*/
|
|
328
|
+
private setupKeyboardShortcuts(): void {
|
|
329
|
+
// Wait for UI to be ready
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
const redactor = this.Blok.UI?.nodes?.redactor;
|
|
332
|
+
|
|
333
|
+
if (!redactor) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const target = this.globalUndoRedo ? document : redactor;
|
|
338
|
+
const shortcutNames = ['CMD+Z', 'CMD+SHIFT+Z', 'CMD+Y'];
|
|
339
|
+
|
|
340
|
+
// Clear any existing undo/redo shortcuts on the target to avoid duplicate registration errors
|
|
341
|
+
shortcutNames.forEach(name => Shortcuts.remove(target, name));
|
|
342
|
+
|
|
343
|
+
// Undo: Cmd+Z (Mac) / Ctrl+Z (Windows/Linux)
|
|
344
|
+
Shortcuts.add({
|
|
345
|
+
name: 'CMD+Z',
|
|
346
|
+
on: target,
|
|
347
|
+
handler: (event: KeyboardEvent) => {
|
|
348
|
+
if (!this.shouldHandleShortcut(event)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
void this.undo();
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
this.registeredShortcuts.push({ name: 'CMD+Z', element: target });
|
|
356
|
+
|
|
357
|
+
// Redo: Cmd+Shift+Z (Mac) / Ctrl+Shift+Z (Windows/Linux)
|
|
358
|
+
Shortcuts.add({
|
|
359
|
+
name: 'CMD+SHIFT+Z',
|
|
360
|
+
on: target,
|
|
361
|
+
handler: (event: KeyboardEvent) => {
|
|
362
|
+
if (!this.shouldHandleShortcut(event)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
void this.redo();
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
this.registeredShortcuts.push({ name: 'CMD+SHIFT+Z', element: target });
|
|
370
|
+
|
|
371
|
+
// Alternative Redo: Cmd+Y (Windows convention)
|
|
372
|
+
Shortcuts.add({
|
|
373
|
+
name: 'CMD+Y',
|
|
374
|
+
on: target,
|
|
375
|
+
handler: (event: KeyboardEvent) => {
|
|
376
|
+
if (!this.shouldHandleShortcut(event)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
void this.redo();
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
this.registeredShortcuts.push({ name: 'CMD+Y', element: target });
|
|
384
|
+
}, 0);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Determines whether this instance should handle the shortcut event
|
|
389
|
+
* @param event - the keyboard event
|
|
390
|
+
* @returns true if this instance should handle the shortcut
|
|
391
|
+
*/
|
|
392
|
+
private shouldHandleShortcut(event: KeyboardEvent): boolean {
|
|
393
|
+
// When using global shortcuts, only the active instance should respond
|
|
394
|
+
if (this.globalUndoRedo && History.activeInstance !== this) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Don't intercept shortcuts when focus is in native form controls outside the editor
|
|
399
|
+
if (this.isNativeFormControl(event.target)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Checks if the target element is a native form control outside the editor
|
|
408
|
+
* @param target - the event target
|
|
409
|
+
* @returns true if target is a form control not within this editor
|
|
410
|
+
*/
|
|
411
|
+
private isNativeFormControl(target: EventTarget | null): boolean {
|
|
412
|
+
if (!(target instanceof HTMLElement)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const editorWrapper = this.Blok.UI?.nodes?.wrapper;
|
|
417
|
+
|
|
418
|
+
// If target is inside the editor, it's not an external form control
|
|
419
|
+
if (editorWrapper?.contains(target)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check for native form controls
|
|
424
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check for contenteditable elements outside the editor
|
|
429
|
+
if (target.isContentEditable) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Handles block mutation events
|
|
438
|
+
* Debounces rapid changes and records state snapshots
|
|
439
|
+
*/
|
|
440
|
+
private handleBlockMutation(_event: BlockMutationEvent): void {
|
|
441
|
+
// Mark this instance as active for global shortcuts
|
|
442
|
+
History.activeInstance = this;
|
|
443
|
+
|
|
444
|
+
// Don't record changes during undo/redo operations
|
|
445
|
+
if (this.isPerformingUndoRedo) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Ensure initial state is captured
|
|
450
|
+
if (!this.initialStateCaptured) {
|
|
451
|
+
void this.captureInitialState();
|
|
452
|
+
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Clear existing debounce timeout
|
|
457
|
+
this.clearDebounce();
|
|
458
|
+
|
|
459
|
+
// Debounce to batch rapid changes
|
|
460
|
+
this.debounceTimeout = setTimeout(() => {
|
|
461
|
+
void this.recordState();
|
|
462
|
+
}, this.debounceTime);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Records the current state to history
|
|
467
|
+
*/
|
|
468
|
+
private async recordState(): Promise<void> {
|
|
469
|
+
// Double-check we're not in undo/redo mode
|
|
470
|
+
if (this.isPerformingUndoRedo) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Clean up any fake background elements before capturing state,
|
|
475
|
+
// UNLESS they are actively being used (e.g., by inline link tool).
|
|
476
|
+
// This ensures fake background spans are never persisted to history,
|
|
477
|
+
// but also preserves the visual selection when inline tools are active.
|
|
478
|
+
const inlineToolInputFocused = document.activeElement?.hasAttribute('data-blok-testid') &&
|
|
479
|
+
document.activeElement?.getAttribute('data-blok-testid') === 'inline-tool-input';
|
|
480
|
+
|
|
481
|
+
if (!inlineToolInputFocused) {
|
|
482
|
+
this.Blok.SelectionAPI.methods.clearFakeBackground();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const state = await this.getCurrentState();
|
|
486
|
+
|
|
487
|
+
if (!state) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Capture caret position along with state
|
|
492
|
+
const caretPosition = this.getCaretPosition();
|
|
493
|
+
|
|
494
|
+
// Clear redo stack when new changes are made
|
|
495
|
+
this.redoStack = [];
|
|
496
|
+
|
|
497
|
+
// Add new entry
|
|
498
|
+
this.undoStack.push({
|
|
499
|
+
state,
|
|
500
|
+
timestamp: Date.now(),
|
|
501
|
+
caretPosition,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Trim stack if exceeds max length
|
|
505
|
+
while (this.undoStack.length > this.maxHistoryLength) {
|
|
506
|
+
this.undoStack.shift();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.emitStateChanged();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Gets current document state without sanitization
|
|
514
|
+
* This captures raw block data for history to preserve inline formatting
|
|
515
|
+
*/
|
|
516
|
+
private async getCurrentState(): Promise<OutputData | null> {
|
|
517
|
+
try {
|
|
518
|
+
const { BlockManager } = this.Blok;
|
|
519
|
+
const blocks = BlockManager.blocks;
|
|
520
|
+
|
|
521
|
+
// If there is only one block and it is empty, return empty blocks array
|
|
522
|
+
if (blocks.length === 1 && blocks[0].isEmpty) {
|
|
523
|
+
return {
|
|
524
|
+
time: Date.now(),
|
|
525
|
+
blocks: [],
|
|
526
|
+
version: '',
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const blockPromises = blocks.map(async (block): Promise<OutputBlockData | null> => {
|
|
531
|
+
const savedData = await block.save();
|
|
532
|
+
|
|
533
|
+
if (!savedData || savedData.data === undefined) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const isValid = await block.validate(savedData.data);
|
|
538
|
+
|
|
539
|
+
if (!isValid) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
id: savedData.id,
|
|
545
|
+
type: savedData.tool,
|
|
546
|
+
data: savedData.data,
|
|
547
|
+
...(savedData.tunes && Object.keys(savedData.tunes).length > 0 && { tunes: savedData.tunes }),
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const results = await Promise.all(blockPromises);
|
|
552
|
+
const validBlocks = results.filter((block): block is OutputBlockData => block !== null);
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
time: Date.now(),
|
|
556
|
+
blocks: validBlocks,
|
|
557
|
+
version: '',
|
|
558
|
+
};
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Restores document to a given state using smart diffing
|
|
566
|
+
* Only updates blocks that have changed to preserve DOM state
|
|
567
|
+
* @param state - the document state to restore
|
|
568
|
+
* @param caretPosition - optional caret position to restore after state is applied
|
|
569
|
+
* @param fallbackBlockIndex - optional block index to use when caret block no longer exists
|
|
570
|
+
* @param fallbackCaretPosition - optional caret position to use when the target state has no caret position
|
|
571
|
+
*/
|
|
572
|
+
private async restoreState(
|
|
573
|
+
state: OutputData,
|
|
574
|
+
caretPosition?: CaretPosition,
|
|
575
|
+
fallbackBlockIndex?: number,
|
|
576
|
+
fallbackCaretPosition?: CaretPosition
|
|
577
|
+
): Promise<void> {
|
|
578
|
+
// Disable modifications observer during restore
|
|
579
|
+
this.Blok.ModificationsObserver.disable();
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
await this.applyStateDiff(state);
|
|
583
|
+
} finally {
|
|
584
|
+
this.Blok.ModificationsObserver.enable();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Restore caret position after state is applied
|
|
588
|
+
this.restoreCaretPosition(caretPosition, fallbackBlockIndex, fallbackCaretPosition);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Apply state changes using diff-based approach
|
|
593
|
+
* This minimizes DOM changes and preserves focus/selection
|
|
594
|
+
*/
|
|
595
|
+
private async applyStateDiff(targetState: OutputData): Promise<void> {
|
|
596
|
+
const { BlockManager, Renderer } = this.Blok;
|
|
597
|
+
const currentBlocks = BlockManager.blocks;
|
|
598
|
+
const targetBlocks = targetState.blocks;
|
|
599
|
+
|
|
600
|
+
// Build maps for quick lookup
|
|
601
|
+
const currentBlocksById = new Map<string, { block: typeof currentBlocks[0]; index: number }>();
|
|
602
|
+
|
|
603
|
+
currentBlocks.forEach((block, index) => {
|
|
604
|
+
currentBlocksById.set(block.id, { block, index });
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const targetBlocksById = new Map<string, { data: OutputBlockData; index: number }>();
|
|
608
|
+
|
|
609
|
+
targetBlocks.forEach((blockData, index) => {
|
|
610
|
+
if (blockData.id) {
|
|
611
|
+
targetBlocksById.set(blockData.id, { data: blockData, index });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Find blocks to remove (exist in current but not in target)
|
|
616
|
+
const blocksToRemove: typeof currentBlocks[0][] = [];
|
|
617
|
+
|
|
618
|
+
for (const block of currentBlocks) {
|
|
619
|
+
if (!targetBlocksById.has(block.id)) {
|
|
620
|
+
blocksToRemove.push(block);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Find blocks to add (exist in target but not in current)
|
|
625
|
+
const blocksToAdd: { data: OutputBlockData; index: number }[] = [];
|
|
626
|
+
|
|
627
|
+
for (const [id, { data, index }] of targetBlocksById) {
|
|
628
|
+
if (!currentBlocksById.has(id)) {
|
|
629
|
+
blocksToAdd.push({ data, index });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Find blocks to update (exist in both but may have changed data)
|
|
634
|
+
const blocksToUpdate: { block: typeof currentBlocks[0]; data: OutputBlockData; targetIndex: number }[] = [];
|
|
635
|
+
|
|
636
|
+
for (const [id, { data, index: targetIndex }] of targetBlocksById) {
|
|
637
|
+
const current = currentBlocksById.get(id);
|
|
638
|
+
|
|
639
|
+
if (current) {
|
|
640
|
+
blocksToUpdate.push({ block: current.block, data, targetIndex });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// If the structure changed significantly, fall back to full re-render
|
|
645
|
+
// This threshold can be adjusted based on performance needs
|
|
646
|
+
const totalChanges = blocksToRemove.length + blocksToAdd.length;
|
|
647
|
+
const significantChange = totalChanges > currentBlocks.length / 2 || totalChanges > 5;
|
|
648
|
+
|
|
649
|
+
if (significantChange || currentBlocks.length === 0) {
|
|
650
|
+
// Full re-render for significant changes
|
|
651
|
+
await BlockManager.clear();
|
|
652
|
+
await Renderer.render(targetBlocks);
|
|
653
|
+
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Apply incremental changes
|
|
658
|
+
|
|
659
|
+
// 1. Remove blocks that no longer exist
|
|
660
|
+
for (const block of blocksToRemove) {
|
|
661
|
+
await BlockManager.removeBlock(block);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 2. Update existing blocks with new data (in-place when possible)
|
|
665
|
+
for (const { block, data } of blocksToUpdate) {
|
|
666
|
+
// Check if data actually changed
|
|
667
|
+
const currentData = await block.data;
|
|
668
|
+
const dataChanged = JSON.stringify(currentData) !== JSON.stringify(data.data);
|
|
669
|
+
|
|
670
|
+
if (!dataChanged) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Try in-place update first to preserve DOM and focus
|
|
675
|
+
const updated = await block.setData(data.data);
|
|
676
|
+
|
|
677
|
+
// Fall back to full re-render if in-place update not supported
|
|
678
|
+
if (!updated) {
|
|
679
|
+
await BlockManager.update(block, data.data, data.tunes);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 3. Add new blocks
|
|
684
|
+
for (const { data, index } of blocksToAdd) {
|
|
685
|
+
BlockManager.insert({
|
|
686
|
+
id: data.id,
|
|
687
|
+
tool: data.type,
|
|
688
|
+
data: data.data,
|
|
689
|
+
index,
|
|
690
|
+
needToFocus: false,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 4. Reorder blocks if needed
|
|
695
|
+
await this.reorderBlocks(targetBlocks);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Reorder blocks to match target order
|
|
700
|
+
*/
|
|
701
|
+
private async reorderBlocks(targetBlocks: OutputBlockData[]): Promise<void> {
|
|
702
|
+
const { BlockManager } = this.Blok;
|
|
703
|
+
|
|
704
|
+
// Create target order map
|
|
705
|
+
const targetOrder = new Map<string, number>();
|
|
706
|
+
|
|
707
|
+
targetBlocks.forEach((block, index) => {
|
|
708
|
+
if (block.id) {
|
|
709
|
+
targetOrder.set(block.id, index);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Get current blocks and their indices
|
|
714
|
+
const currentBlocks = BlockManager.blocks;
|
|
715
|
+
|
|
716
|
+
// Check if reordering is needed by comparing positions
|
|
717
|
+
const needsReorder = currentBlocks.some((block, i) => {
|
|
718
|
+
const targetIndex = targetOrder.get(block.id);
|
|
719
|
+
|
|
720
|
+
return targetIndex !== undefined && targetIndex !== i;
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
if (!needsReorder) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Apply moves to get blocks in correct order
|
|
728
|
+
// We iterate from the end to avoid index shifting issues
|
|
729
|
+
targetBlocks.forEach((targetBlock, targetIndex) => {
|
|
730
|
+
const targetBlockId = targetBlock.id;
|
|
731
|
+
|
|
732
|
+
if (!targetBlockId) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const currentIndex = BlockManager.blocks.findIndex(b => b.id === targetBlockId);
|
|
737
|
+
|
|
738
|
+
if (currentIndex !== -1 && currentIndex !== targetIndex) {
|
|
739
|
+
BlockManager.move(targetIndex, currentIndex);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Wait for a short cooldown period
|
|
746
|
+
* This helps ignore late-firing events after state restore
|
|
747
|
+
*/
|
|
748
|
+
private cooldown(): Promise<void> {
|
|
749
|
+
return new Promise(resolve => setTimeout(resolve, RESTORE_COOLDOWN_TIME));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Captures current caret position for later restoration
|
|
754
|
+
* @returns CaretPosition or undefined if caret is not in the editor
|
|
755
|
+
*/
|
|
756
|
+
private getCaretPosition(): CaretPosition | undefined {
|
|
757
|
+
const { BlockManager } = this.Blok;
|
|
758
|
+
const currentBlock = BlockManager.currentBlock;
|
|
759
|
+
|
|
760
|
+
if (!currentBlock) {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const currentInput = currentBlock.currentInput;
|
|
765
|
+
|
|
766
|
+
if (!currentInput) {
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const inputIndex = currentBlock.inputs.indexOf(currentInput);
|
|
771
|
+
const offset = this.getCaretOffset(currentInput);
|
|
772
|
+
const blockIndex = BlockManager.currentBlockIndex;
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
blockId: currentBlock.id,
|
|
776
|
+
blockIndex: blockIndex >= 0 ? blockIndex : 0,
|
|
777
|
+
inputIndex: inputIndex >= 0 ? inputIndex : 0,
|
|
778
|
+
offset,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Gets caret offset within an input element
|
|
784
|
+
* @param input - the input element
|
|
785
|
+
* @returns character offset
|
|
786
|
+
*/
|
|
787
|
+
private getCaretOffset(input: HTMLElement): number {
|
|
788
|
+
const selection = window.getSelection();
|
|
789
|
+
|
|
790
|
+
if (!selection || selection.rangeCount === 0) {
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const range = selection.getRangeAt(0);
|
|
795
|
+
|
|
796
|
+
// Check if selection is within this input
|
|
797
|
+
if (!input.contains(range.startContainer)) {
|
|
798
|
+
return 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// For native inputs, use selectionStart
|
|
802
|
+
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
|
|
803
|
+
return input.selectionStart ?? 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// For contenteditable, calculate offset by creating a range from start to caret
|
|
807
|
+
try {
|
|
808
|
+
const preCaretRange = document.createRange();
|
|
809
|
+
|
|
810
|
+
preCaretRange.selectNodeContents(input);
|
|
811
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
812
|
+
|
|
813
|
+
return preCaretRange.toString().length;
|
|
814
|
+
} catch {
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Restores caret to a previously saved position
|
|
821
|
+
* @param caretPosition - the position to restore
|
|
822
|
+
* @param fallbackBlockIndex - optional block index to use when caret block no longer exists
|
|
823
|
+
* @param fallbackCaretPosition - optional caret position to use when the target state has no caret position
|
|
824
|
+
*/
|
|
825
|
+
private restoreCaretPosition(
|
|
826
|
+
caretPosition: CaretPosition | undefined,
|
|
827
|
+
fallbackBlockIndex?: number,
|
|
828
|
+
fallbackCaretPosition?: CaretPosition
|
|
829
|
+
): void {
|
|
830
|
+
// If no caret position but have fallback caret position, use it
|
|
831
|
+
if (!caretPosition && fallbackCaretPosition) {
|
|
832
|
+
// Try to use the fallback caret position's block index and input index
|
|
833
|
+
// The block ID might not exist in the restored state, so we use the index as fallback
|
|
834
|
+
this.focusBlockAtIndexWithInput(
|
|
835
|
+
fallbackCaretPosition.blockIndex,
|
|
836
|
+
fallbackCaretPosition.inputIndex,
|
|
837
|
+
fallbackCaretPosition.offset
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// If no caret position but have fallback block index, use it
|
|
844
|
+
if (!caretPosition && fallbackBlockIndex !== undefined) {
|
|
845
|
+
this.focusBlockAtIndex(fallbackBlockIndex);
|
|
846
|
+
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// No saved caret position and no fallback, focus first available block
|
|
851
|
+
if (!caretPosition) {
|
|
852
|
+
this.focusFirstAvailableBlock();
|
|
853
|
+
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const { BlockManager } = this.Blok;
|
|
858
|
+
|
|
859
|
+
// Look up block by ID. Note: we need to look up the block again after waiting
|
|
860
|
+
// because the block instance might have been replaced during state restoration
|
|
861
|
+
// (e.g., BlockManager.update creates a new block with the same ID)
|
|
862
|
+
const block = BlockManager.getBlockById(caretPosition.blockId);
|
|
863
|
+
|
|
864
|
+
if (!block) {
|
|
865
|
+
// Block no longer exists, use fallback index (preceding block) or saved index
|
|
866
|
+
const indexToUse = fallbackBlockIndex !== undefined ? fallbackBlockIndex : caretPosition.blockIndex;
|
|
867
|
+
|
|
868
|
+
this.focusBlockAtIndex(indexToUse);
|
|
869
|
+
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Wait for block to be ready (in case it was just rendered)
|
|
874
|
+
// Then look up the block again by ID to get the current instance
|
|
875
|
+
// This is necessary because the block might have been replaced during rendering
|
|
876
|
+
void block.ready.then(() => {
|
|
877
|
+
// Re-lookup the block by ID to get the current instance
|
|
878
|
+
// The original block reference might be stale if the block was replaced
|
|
879
|
+
const currentBlock = BlockManager.getBlockById(caretPosition.blockId);
|
|
880
|
+
|
|
881
|
+
if (!currentBlock) {
|
|
882
|
+
// Block was removed during rendering, use fallback
|
|
883
|
+
const indexToUse = fallbackBlockIndex !== undefined ? fallbackBlockIndex : caretPosition.blockIndex;
|
|
884
|
+
|
|
885
|
+
this.focusBlockAtIndex(indexToUse);
|
|
886
|
+
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// If the block was replaced, wait for the new block to be ready
|
|
891
|
+
if (currentBlock !== block) {
|
|
892
|
+
void currentBlock.ready.then(() => {
|
|
893
|
+
this.setCaretToBlockInput(currentBlock, caretPosition);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
this.setCaretToBlockInput(currentBlock, caretPosition);
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Focuses the first available focusable block in the editor
|
|
905
|
+
*/
|
|
906
|
+
private focusFirstAvailableBlock(): void {
|
|
907
|
+
const { BlockManager, Caret } = this.Blok;
|
|
908
|
+
const firstBlock = BlockManager.firstBlock;
|
|
909
|
+
|
|
910
|
+
if (firstBlock?.focusable) {
|
|
911
|
+
Caret.setToBlock(firstBlock, Caret.positions.END);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Focuses the block at the given index, or the last available block if index is out of bounds
|
|
917
|
+
* @param index - the target block index
|
|
918
|
+
*/
|
|
919
|
+
private focusBlockAtIndex(index: number): void {
|
|
920
|
+
const { BlockManager, Caret } = this.Blok;
|
|
921
|
+
const blocksCount = BlockManager.blocks.length;
|
|
922
|
+
|
|
923
|
+
if (blocksCount === 0) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Use the block at the saved index, or the last block if index is out of bounds
|
|
928
|
+
const targetIndex = Math.min(index, blocksCount - 1);
|
|
929
|
+
const targetBlock = BlockManager.getBlockByIndex(targetIndex);
|
|
930
|
+
|
|
931
|
+
if (targetBlock?.focusable) {
|
|
932
|
+
Caret.setToBlock(targetBlock, Caret.positions.END);
|
|
933
|
+
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Fallback to last block if target is not focusable
|
|
938
|
+
const lastBlock = BlockManager.lastBlock;
|
|
939
|
+
|
|
940
|
+
if (lastBlock?.focusable) {
|
|
941
|
+
Caret.setToBlock(lastBlock, Caret.positions.END);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Focuses the block at the given index with a specific input and offset
|
|
947
|
+
* Used when restoring caret position from a fallback that has input/offset info
|
|
948
|
+
* @param blockIndex - the target block index
|
|
949
|
+
* @param inputIndex - the target input index within the block
|
|
950
|
+
* @param offset - the character offset within the input
|
|
951
|
+
*/
|
|
952
|
+
private focusBlockAtIndexWithInput(blockIndex: number, inputIndex: number, offset: number): void {
|
|
953
|
+
const { BlockManager, Caret } = this.Blok;
|
|
954
|
+
const blocksCount = BlockManager.blocks.length;
|
|
955
|
+
|
|
956
|
+
if (blocksCount === 0) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Check if the block index is out of bounds
|
|
961
|
+
const isOutOfBounds = blockIndex >= blocksCount;
|
|
962
|
+
|
|
963
|
+
// Use the block at the saved index, or the last block if index is out of bounds
|
|
964
|
+
const targetIndex = Math.min(blockIndex, blocksCount - 1);
|
|
965
|
+
const targetBlock = BlockManager.getBlockByIndex(targetIndex);
|
|
966
|
+
|
|
967
|
+
if (!targetBlock?.focusable) {
|
|
968
|
+
this.focusFirstAvailableBlock();
|
|
969
|
+
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// If the block index was out of bounds, set caret to the END of the block
|
|
974
|
+
// because the original block no longer exists
|
|
975
|
+
if (isOutOfBounds) {
|
|
976
|
+
void targetBlock.ready.then(() => {
|
|
977
|
+
Caret.setToBlock(targetBlock, Caret.positions.END);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Create a synthetic caret position to use with setCaretToBlockInput
|
|
984
|
+
const syntheticCaretPosition: CaretPosition = {
|
|
985
|
+
blockId: targetBlock.id,
|
|
986
|
+
blockIndex: targetIndex,
|
|
987
|
+
inputIndex,
|
|
988
|
+
offset,
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// Wait for block to be ready and set caret
|
|
992
|
+
void targetBlock.ready.then(() => {
|
|
993
|
+
this.setCaretToBlockInput(targetBlock, syntheticCaretPosition);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Sets caret to a specific input within a block
|
|
999
|
+
* @param block - the block to set caret in
|
|
1000
|
+
* @param caretPosition - the saved caret position
|
|
1001
|
+
*/
|
|
1002
|
+
private setCaretToBlockInput(block: Block, caretPosition: CaretPosition): void {
|
|
1003
|
+
const { BlockManager, Caret } = this.Blok;
|
|
1004
|
+
|
|
1005
|
+
// Use requestAnimationFrame to ensure the DOM has been updated
|
|
1006
|
+
// This is necessary because the block's inputs might not be available immediately
|
|
1007
|
+
// after the block is rendered, especially for complex tools like lists
|
|
1008
|
+
requestAnimationFrame(() => {
|
|
1009
|
+
const inputs = block.inputs;
|
|
1010
|
+
const targetInputIndex = Math.min(caretPosition.inputIndex, inputs.length - 1);
|
|
1011
|
+
const targetInput = inputs[targetInputIndex];
|
|
1012
|
+
|
|
1013
|
+
if (!targetInput) {
|
|
1014
|
+
// No inputs, just select the block
|
|
1015
|
+
Caret.setToBlock(block, Caret.positions.END);
|
|
1016
|
+
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Set current block and let Caret.setToInput handle the input assignment
|
|
1021
|
+
BlockManager.currentBlock = block;
|
|
1022
|
+
|
|
1023
|
+
// Try to set exact offset, fall back to end if offset is out of bounds
|
|
1024
|
+
try {
|
|
1025
|
+
Caret.setToInput(targetInput, Caret.positions.DEFAULT, caretPosition.offset);
|
|
1026
|
+
} catch {
|
|
1027
|
+
Caret.setToInput(targetInput, Caret.positions.END);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Clears the debounce timeout
|
|
1034
|
+
*/
|
|
1035
|
+
private clearDebounce(): void {
|
|
1036
|
+
if (this.debounceTimeout) {
|
|
1037
|
+
clearTimeout(this.debounceTimeout);
|
|
1038
|
+
this.debounceTimeout = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Preserves the current caret position by re-focusing the current input
|
|
1044
|
+
* Used when undo/redo has nothing to do but we want to prevent caret from moving
|
|
1045
|
+
*/
|
|
1046
|
+
private preserveCaretPosition(): void {
|
|
1047
|
+
const { BlockManager, Caret } = this.Blok;
|
|
1048
|
+
const currentBlock = BlockManager.currentBlock;
|
|
1049
|
+
|
|
1050
|
+
if (!currentBlock?.focusable) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const currentInput = currentBlock.currentInput;
|
|
1055
|
+
|
|
1056
|
+
if (currentInput) {
|
|
1057
|
+
// Re-focus the current input to ensure caret stays in place
|
|
1058
|
+
currentInput.focus();
|
|
1059
|
+
} else {
|
|
1060
|
+
// Fallback to setting caret to the block
|
|
1061
|
+
Caret.setToBlock(currentBlock, Caret.positions.END);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Emits history state changed event
|
|
1067
|
+
*/
|
|
1068
|
+
private emitStateChanged(): void {
|
|
1069
|
+
this.eventsDispatcher.emit(HistoryStateChanged, {
|
|
1070
|
+
canUndo: this.canUndo(),
|
|
1071
|
+
canRedo: this.canRedo(),
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Cleans up history module resources
|
|
1077
|
+
* Removes shortcuts and clears state
|
|
1078
|
+
*/
|
|
1079
|
+
public destroy(): void {
|
|
1080
|
+
this.clearDebounce();
|
|
1081
|
+
|
|
1082
|
+
// Remove registered shortcuts
|
|
1083
|
+
for (const { name, element } of this.registeredShortcuts) {
|
|
1084
|
+
Shortcuts.remove(element, name);
|
|
1085
|
+
}
|
|
1086
|
+
this.registeredShortcuts = [];
|
|
1087
|
+
|
|
1088
|
+
// Clear active instance if it's this one
|
|
1089
|
+
if (History.activeInstance === this) {
|
|
1090
|
+
History.activeInstance = null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Clear stacks
|
|
1094
|
+
this.undoStack = [];
|
|
1095
|
+
this.redoStack = [];
|
|
1096
|
+
this.initialStateCaptured = false;
|
|
1097
|
+
}
|
|
1098
|
+
}
|