@jackuait/blok 0.6.0-beta.9 → 0.7.0-beta.1
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/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-Bn6Q_o8h.mjs → blok-ob9Fwr1L.mjs} +3414 -2975
- package/dist/chunks/i18next-B47TKgbU.mjs +1303 -0
- package/dist/chunks/{i18next-loader-DjR4d8M7.mjs → i18next-loader-Bu3vFvye.mjs} +2 -2
- package/dist/chunks/{index-oe38cp86.mjs → index-CZmRzRIX.mjs} +12 -12
- package/dist/chunks/{inline-tool-convert-SRTkyaZn.mjs → inline-tool-convert-CvFW2iie.mjs} +1579 -961
- package/dist/chunks/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
- package/dist/chunks/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
- package/dist/{messages-DnXLrlHh.mjs → chunks/messages-4v4MuVEc.mjs} +6 -0
- package/dist/chunks/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
- package/dist/chunks/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
- package/dist/chunks/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
- package/dist/{messages-uKX8WBaD.mjs → chunks/messages-96kNZDll.mjs} +6 -0
- package/dist/chunks/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
- package/dist/{messages-DBn76jVV.mjs → chunks/messages-B217znr-.mjs} +8 -2
- package/dist/{messages-DT4dP5uK.mjs → chunks/messages-B8WNljW3.mjs} +6 -0
- package/dist/chunks/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
- package/dist/{messages-CZygwLwM.mjs → chunks/messages-BI43k_BD.mjs} +6 -0
- package/dist/{messages-CzTufCHu.mjs → chunks/messages-BJ6zrz2j.mjs} +6 -0
- package/dist/{messages-BoJc_p1r.mjs → chunks/messages-BUl_Rcnj.mjs} +6 -0
- package/dist/chunks/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
- package/dist/{messages-C2htQ_3F.mjs → chunks/messages-BcpCubnC.mjs} +6 -0
- package/dist/{messages-D5C3J9qr.mjs → chunks/messages-Bm-E4iRC.mjs} +6 -0
- package/dist/chunks/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
- package/dist/chunks/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
- package/dist/chunks/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
- package/dist/{messages-Dz9L52ol.mjs → chunks/messages-CRJ_mchV.mjs} +6 -0
- package/dist/chunks/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
- package/dist/chunks/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
- package/dist/{messages-R3hUSvr3.mjs → chunks/messages-CfiyT2Wi.mjs} +6 -0
- package/dist/{messages-CJdUsQ-c.mjs → chunks/messages-CgTq3QhU.mjs} +6 -0
- package/dist/chunks/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
- package/dist/{messages-Q7AO_FLv.mjs → chunks/messages-Cjjo7yHR.mjs} +6 -0
- package/dist/{messages-C99mq906.mjs → chunks/messages-Cl6ayUaq.mjs} +6 -0
- package/dist/chunks/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
- package/dist/chunks/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
- package/dist/chunks/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
- package/dist/{messages-BWF-zUpY.mjs → chunks/messages-CrCYPCk3.mjs} +6 -0
- package/dist/{messages-D-ZtY5v0.mjs → chunks/messages-Cs8zmZ3L.mjs} +6 -0
- package/dist/{messages-DprmQg6V.mjs → chunks/messages-CzK0LEhb.mjs} +6 -0
- package/dist/chunks/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
- package/dist/chunks/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
- package/dist/chunks/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
- package/dist/{messages-kep5wtm4.mjs → chunks/messages-D4qqwVgQ.mjs} +6 -0
- package/dist/chunks/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
- package/dist/{messages-CY8_RyFE.mjs → chunks/messages-D7u2bmP2.mjs} +6 -0
- package/dist/chunks/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
- package/dist/{messages-DLfR5bMd.mjs → chunks/messages-D9ndgBnU.mjs} +6 -0
- package/dist/{messages-CVw84KdI.mjs → chunks/messages-DDTQgImT.mjs} +6 -0
- package/dist/{messages-_ErNTNhk.mjs → chunks/messages-DH_jBeED.mjs} +6 -0
- package/dist/chunks/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
- package/dist/chunks/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
- package/dist/chunks/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
- package/dist/chunks/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
- package/dist/chunks/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
- package/dist/chunks/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
- package/dist/{messages-Z9nEU2xK.mjs → chunks/messages-DviiFSv2.mjs} +6 -0
- package/dist/chunks/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
- package/dist/{messages-w7v1GNaE.mjs → chunks/messages-Dzzn6XoD.mjs} +6 -0
- package/dist/{messages-CqWJcCbY.mjs → chunks/messages-GSByFygY.mjs} +6 -0
- package/dist/chunks/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
- package/dist/chunks/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
- package/dist/{messages-BU2nlrLK.mjs → chunks/messages-RonBBCnh.mjs} +6 -0
- package/dist/{messages-Bmu_S7GM.mjs → chunks/messages-VDriF5Qy.mjs} +6 -0
- package/dist/{messages-CLhcMlTc.mjs → chunks/messages-ZjUAIWb1.mjs} +6 -0
- package/dist/{messages-9SihnaXQ.mjs → chunks/messages-b1EdvUm0.mjs} +6 -0
- package/dist/{messages-DvFLX36Q.mjs → chunks/messages-begYOTgC.mjs} +6 -0
- package/dist/{messages-BMv4xwIr.mjs → chunks/messages-jrncnb-H.mjs} +6 -0
- package/dist/{messages-D5iv1Kox.mjs → chunks/messages-nefz1S71.mjs} +6 -0
- package/dist/{messages-CQwpzUFp.mjs → chunks/messages-ucTVgS5G.mjs} +6 -0
- package/dist/chunks/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
- package/dist/{messages-C9eaarcK.mjs → chunks/messages-wmi-iFkH.mjs} +6 -0
- package/dist/chunks/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
- package/dist/full.mjs +30 -27
- package/dist/locales.mjs +90 -84
- package/dist/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
- package/dist/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
- package/dist/{chunks/messages-DnXLrlHh.mjs → messages-4v4MuVEc.mjs} +6 -0
- package/dist/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
- package/dist/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
- package/dist/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
- package/dist/{chunks/messages-uKX8WBaD.mjs → messages-96kNZDll.mjs} +6 -0
- package/dist/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
- package/dist/{chunks/messages-DBn76jVV.mjs → messages-B217znr-.mjs} +8 -2
- package/dist/{chunks/messages-DT4dP5uK.mjs → messages-B8WNljW3.mjs} +6 -0
- package/dist/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
- package/dist/{chunks/messages-CZygwLwM.mjs → messages-BI43k_BD.mjs} +6 -0
- package/dist/{chunks/messages-CzTufCHu.mjs → messages-BJ6zrz2j.mjs} +6 -0
- package/dist/{chunks/messages-BoJc_p1r.mjs → messages-BUl_Rcnj.mjs} +6 -0
- package/dist/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
- package/dist/{chunks/messages-C2htQ_3F.mjs → messages-BcpCubnC.mjs} +6 -0
- package/dist/{chunks/messages-D5C3J9qr.mjs → messages-Bm-E4iRC.mjs} +6 -0
- package/dist/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
- package/dist/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
- package/dist/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
- package/dist/{chunks/messages-Dz9L52ol.mjs → messages-CRJ_mchV.mjs} +6 -0
- package/dist/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
- package/dist/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
- package/dist/{chunks/messages-R3hUSvr3.mjs → messages-CfiyT2Wi.mjs} +6 -0
- package/dist/{chunks/messages-CJdUsQ-c.mjs → messages-CgTq3QhU.mjs} +6 -0
- package/dist/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
- package/dist/{chunks/messages-Q7AO_FLv.mjs → messages-Cjjo7yHR.mjs} +6 -0
- package/dist/{chunks/messages-C99mq906.mjs → messages-Cl6ayUaq.mjs} +6 -0
- package/dist/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
- package/dist/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
- package/dist/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
- package/dist/{chunks/messages-BWF-zUpY.mjs → messages-CrCYPCk3.mjs} +6 -0
- package/dist/{chunks/messages-D-ZtY5v0.mjs → messages-Cs8zmZ3L.mjs} +6 -0
- package/dist/{chunks/messages-DprmQg6V.mjs → messages-CzK0LEhb.mjs} +6 -0
- package/dist/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
- package/dist/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
- package/dist/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
- package/dist/{chunks/messages-kep5wtm4.mjs → messages-D4qqwVgQ.mjs} +6 -0
- package/dist/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
- package/dist/{chunks/messages-CY8_RyFE.mjs → messages-D7u2bmP2.mjs} +6 -0
- package/dist/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
- package/dist/{chunks/messages-DLfR5bMd.mjs → messages-D9ndgBnU.mjs} +6 -0
- package/dist/{chunks/messages-CVw84KdI.mjs → messages-DDTQgImT.mjs} +6 -0
- package/dist/{chunks/messages-_ErNTNhk.mjs → messages-DH_jBeED.mjs} +6 -0
- package/dist/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
- package/dist/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
- package/dist/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
- package/dist/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
- package/dist/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
- package/dist/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
- package/dist/{chunks/messages-Z9nEU2xK.mjs → messages-DviiFSv2.mjs} +6 -0
- package/dist/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
- package/dist/{chunks/messages-w7v1GNaE.mjs → messages-Dzzn6XoD.mjs} +6 -0
- package/dist/{chunks/messages-CqWJcCbY.mjs → messages-GSByFygY.mjs} +6 -0
- package/dist/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
- package/dist/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
- package/dist/{chunks/messages-BU2nlrLK.mjs → messages-RonBBCnh.mjs} +6 -0
- package/dist/{chunks/messages-Bmu_S7GM.mjs → messages-VDriF5Qy.mjs} +6 -0
- package/dist/{chunks/messages-CLhcMlTc.mjs → messages-ZjUAIWb1.mjs} +6 -0
- package/dist/{chunks/messages-9SihnaXQ.mjs → messages-b1EdvUm0.mjs} +6 -0
- package/dist/{chunks/messages-DvFLX36Q.mjs → messages-begYOTgC.mjs} +6 -0
- package/dist/{chunks/messages-BMv4xwIr.mjs → messages-jrncnb-H.mjs} +6 -0
- package/dist/{chunks/messages-D5iv1Kox.mjs → messages-nefz1S71.mjs} +6 -0
- package/dist/{chunks/messages-CQwpzUFp.mjs → messages-ucTVgS5G.mjs} +6 -0
- package/dist/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
- package/dist/{chunks/messages-C9eaarcK.mjs → messages-wmi-iFkH.mjs} +6 -0
- package/dist/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
- package/dist/tools.mjs +3537 -1710
- package/dist/vendor.LICENSE.txt +109 -109
- package/package.json +43 -57
- package/src/blok.ts +12 -0
- package/src/components/__module.ts +21 -0
- package/src/components/block/api.ts +17 -0
- package/src/components/block/style-manager.ts +6 -2
- package/src/components/block/tool-renderer.ts +33 -30
- package/src/components/blocks.ts +132 -15
- package/src/components/constants/data-attributes.ts +7 -0
- package/src/components/i18n/locales/am/messages.json +6 -0
- package/src/components/i18n/locales/ar/messages.json +6 -0
- package/src/components/i18n/locales/az/messages.json +6 -0
- package/src/components/i18n/locales/bg/messages.json +6 -0
- package/src/components/i18n/locales/bn/messages.json +6 -0
- package/src/components/i18n/locales/bs/messages.json +6 -0
- package/src/components/i18n/locales/cs/messages.json +6 -0
- package/src/components/i18n/locales/da/messages.json +6 -0
- package/src/components/i18n/locales/de/messages.json +6 -0
- package/src/components/i18n/locales/dv/messages.json +6 -0
- package/src/components/i18n/locales/el/messages.json +6 -0
- package/src/components/i18n/locales/en/messages.json +6 -0
- package/src/components/i18n/locales/es/messages.json +6 -0
- package/src/components/i18n/locales/et/messages.json +6 -0
- package/src/components/i18n/locales/fa/messages.json +6 -0
- package/src/components/i18n/locales/fi/messages.json +6 -0
- package/src/components/i18n/locales/fil/messages.json +6 -0
- package/src/components/i18n/locales/fr/messages.json +6 -0
- package/src/components/i18n/locales/gu/messages.json +6 -0
- package/src/components/i18n/locales/he/messages.json +6 -0
- package/src/components/i18n/locales/hi/messages.json +6 -0
- package/src/components/i18n/locales/hr/messages.json +6 -0
- package/src/components/i18n/locales/hu/messages.json +6 -0
- package/src/components/i18n/locales/hy/messages.json +6 -0
- package/src/components/i18n/locales/id/messages.json +6 -0
- package/src/components/i18n/locales/it/messages.json +6 -0
- package/src/components/i18n/locales/ja/messages.json +6 -0
- package/src/components/i18n/locales/ka/messages.json +6 -0
- package/src/components/i18n/locales/km/messages.json +6 -0
- package/src/components/i18n/locales/kn/messages.json +6 -0
- package/src/components/i18n/locales/ko/messages.json +6 -0
- package/src/components/i18n/locales/ku/messages.json +6 -0
- package/src/components/i18n/locales/lo/messages.json +6 -0
- package/src/components/i18n/locales/lt/messages.json +6 -0
- package/src/components/i18n/locales/lv/messages.json +6 -0
- package/src/components/i18n/locales/mk/messages.json +6 -0
- package/src/components/i18n/locales/ml/messages.json +6 -0
- package/src/components/i18n/locales/mn/messages.json +6 -0
- package/src/components/i18n/locales/mr/messages.json +6 -0
- package/src/components/i18n/locales/ms/messages.json +6 -0
- package/src/components/i18n/locales/my/messages.json +6 -0
- package/src/components/i18n/locales/ne/messages.json +6 -0
- package/src/components/i18n/locales/nl/messages.json +6 -0
- package/src/components/i18n/locales/no/messages.json +6 -0
- package/src/components/i18n/locales/pa/messages.json +6 -0
- package/src/components/i18n/locales/pl/messages.json +6 -0
- package/src/components/i18n/locales/ps/messages.json +6 -0
- package/src/components/i18n/locales/pt/messages.json +6 -0
- package/src/components/i18n/locales/ro/messages.json +6 -0
- package/src/components/i18n/locales/ru/messages.json +6 -0
- package/src/components/i18n/locales/sd/messages.json +6 -0
- package/src/components/i18n/locales/si/messages.json +6 -0
- package/src/components/i18n/locales/sk/messages.json +6 -0
- package/src/components/i18n/locales/sl/messages.json +6 -0
- package/src/components/i18n/locales/sq/messages.json +6 -0
- package/src/components/i18n/locales/sr/messages.json +6 -0
- package/src/components/i18n/locales/sv/messages.json +6 -0
- package/src/components/i18n/locales/sw/messages.json +6 -0
- package/src/components/i18n/locales/ta/messages.json +6 -0
- package/src/components/i18n/locales/te/messages.json +6 -0
- package/src/components/i18n/locales/th/messages.json +6 -0
- package/src/components/i18n/locales/tr/messages.json +6 -0
- package/src/components/i18n/locales/ug/messages.json +6 -0
- package/src/components/i18n/locales/uk/messages.json +6 -0
- package/src/components/i18n/locales/ur/messages.json +6 -0
- package/src/components/i18n/locales/vi/messages.json +6 -0
- package/src/components/i18n/locales/yi/messages.json +6 -0
- package/src/components/i18n/locales/zh/messages.json +6 -0
- package/src/components/icons/index.ts +61 -7
- package/src/components/inline-tools/inline-tool-link.ts +1 -1
- package/src/components/inline-tools/inline-tool-marker.ts +737 -0
- package/src/components/inline-tools/utils/formatting-range-utils.ts +6 -3
- package/src/components/inline-tools/utils/marker-dom-utils.ts +17 -0
- package/src/components/modules/api/blocks.ts +34 -9
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +75 -29
- package/src/components/modules/blockEvents/composers/markdownShortcuts.ts +54 -2
- package/src/components/modules/blockEvents/constants.ts +12 -0
- package/src/components/modules/blockEvents/index.ts +13 -5
- package/src/components/modules/blockManager/blockManager.ts +81 -2
- package/src/components/modules/blockManager/hierarchy.ts +20 -2
- package/src/components/modules/blockManager/operations.ts +70 -35
- package/src/components/modules/blockManager/repository.ts +22 -0
- package/src/components/modules/blockManager/types.ts +3 -1
- package/src/components/modules/blockManager/yjs-sync.ts +173 -39
- package/src/components/modules/blockSelection.ts +3 -0
- package/src/components/modules/crossBlockSelection.ts +11 -3
- package/src/components/modules/drag/preview/DragPreview.ts +10 -2
- package/src/components/modules/drag/target/DropTargetDetector.ts +100 -11
- package/src/components/modules/drag/utils/drag.constants.ts +1 -1
- package/src/components/modules/normalizeInlineImages.ts +263 -0
- package/src/components/modules/paste/google-docs-preprocessor.ts +197 -0
- package/src/components/modules/paste/handlers/base.ts +43 -2
- package/src/components/modules/paste/handlers/html-handler.ts +1 -1
- package/src/components/modules/paste/handlers/index.ts +1 -0
- package/src/components/modules/paste/handlers/table-cells-handler.ts +104 -0
- package/src/components/modules/paste/index.ts +20 -3
- package/src/components/modules/readonly.ts +8 -2
- package/src/components/modules/rectangleSelection.ts +5 -2
- package/src/components/modules/renderer.ts +35 -0
- package/src/components/modules/saver.ts +52 -2
- package/src/components/modules/toolbar/blockSettings.ts +52 -44
- package/src/components/modules/toolbar/index.ts +124 -17
- package/src/components/modules/toolbar/inline/index.ts +4 -4
- package/src/components/modules/toolbar/plus-button.ts +3 -3
- package/src/components/modules/toolbar/settings-toggler.ts +3 -3
- package/src/components/modules/toolbar/styles.ts +7 -7
- package/src/components/modules/ui.ts +6 -6
- package/src/components/modules/uiControllers/controllers/blockHover.ts +16 -2
- package/src/components/modules/uiControllers/handlers/touch.ts +83 -10
- package/src/components/modules/yjs/block-observer.ts +9 -3
- package/src/components/modules/yjs/document-store.ts +10 -7
- package/src/components/modules/yjs/types.ts +8 -6
- package/src/components/modules/yjs/undo-history.ts +90 -11
- package/src/components/selection/fake-background/shadows.ts +1 -1
- package/src/components/shared/color-picker.ts +211 -0
- package/src/components/shared/color-presets.ts +25 -0
- package/src/components/ui/toolbox.ts +27 -11
- package/src/components/utils/color-mapping.ts +241 -0
- package/src/components/utils/notifier/draw.ts +9 -9
- package/src/components/utils/placeholder.ts +24 -8
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +3 -3
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +15 -12
- package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -2
- package/src/components/utils/popover/popover-abstract.ts +30 -5
- package/src/components/utils/popover/popover-desktop.ts +26 -3
- package/src/components/utils/popover/popover-inline.ts +14 -1
- package/src/components/utils/popover/popover-mobile.ts +4 -4
- package/src/components/utils/popover/popover.const.ts +3 -3
- package/src/components/utils/sanitizer.ts +24 -3
- package/src/components/utils/tw.ts +17 -5
- package/src/full.ts +4 -0
- package/src/stories/Header.stories.ts +106 -0
- package/src/stories/MarkerColors.stories.ts +730 -0
- package/src/stories/Placeholder.stories.ts +7 -2
- package/src/stories/Popover.stories.ts +1 -3
- package/src/stories/Table.stories.ts +1662 -0
- package/src/stories/helpers.ts +2 -0
- package/src/styles/main.css +217 -39
- package/src/tools/header/index.ts +204 -26
- package/src/tools/index.ts +5 -1
- package/src/tools/list/caret-manager.ts +28 -10
- package/src/tools/list/constants.ts +2 -2
- package/src/tools/list/dom-builder.ts +3 -3
- package/src/tools/list/static-configs.ts +0 -1
- package/src/tools/paragraph/index.ts +9 -5
- package/src/tools/table/core/table-commands.ts +99 -0
- package/src/tools/table/core/table-controller.ts +231 -0
- package/src/tools/table/core/table-events.ts +102 -0
- package/src/tools/table/index.ts +1070 -174
- package/src/tools/table/ownership/table-event-broker.ts +74 -0
- package/src/tools/table/ownership/table-ownership-registry.ts +126 -0
- package/src/tools/table/table-add-controls.ts +85 -15
- package/src/tools/table/table-cell-blocks.ts +336 -38
- package/src/tools/table/table-cell-clipboard.ts +415 -0
- package/src/tools/table/table-cell-color-picker.ts +34 -0
- package/src/tools/table/table-cell-selection.ts +264 -15
- package/src/tools/table/table-core.ts +3 -42
- package/src/tools/table/table-heading-toggle.ts +2 -2
- package/src/tools/table/table-model.ts +623 -0
- package/src/tools/table/table-operations.ts +59 -78
- package/src/tools/table/table-resize.ts +15 -11
- package/src/tools/table/table-restrictions.ts +69 -3
- package/src/tools/table/table-row-col-action-handler.ts +22 -7
- package/src/tools/table/table-row-col-controls.ts +129 -12
- package/src/tools/table/table-row-col-drag.ts +14 -0
- package/src/tools/table/table-scroll-haze.ts +152 -0
- package/src/tools/table/types.ts +22 -1
- package/src/tools/table/view/table-cell-blocks-adapter.ts +47 -0
- package/src/tools/toggle/block-operations.ts +110 -0
- package/src/tools/toggle/constants.ts +49 -0
- package/src/tools/toggle/dom-builder.ts +125 -0
- package/src/tools/toggle/index.ts +280 -0
- package/src/tools/toggle/toggle-keyboard.ts +139 -0
- package/src/tools/toggle/toggle-lifecycle.ts +80 -0
- package/src/tools/toggle/toggle-shortcuts.ts +107 -0
- package/src/tools/toggle/types.ts +21 -0
- package/src/variants/blok-minimum.ts +13 -0
- package/types/api/block.d.ts +13 -0
- package/types/api/blocks.d.ts +16 -0
- package/types/full.d.ts +2 -0
- package/types/tools/table.d.ts +2 -0
- package/types/tools-entry.d.ts +2 -1
- package/dist/chunks/i18next-CugVlwWp.mjs +0 -1292
- package/src/tools/table/data-normalizer.ts +0 -32
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InlineTool,
|
|
3
|
+
InlineToolConstructorOptions,
|
|
4
|
+
SanitizerConfig
|
|
5
|
+
} from '../../../types';
|
|
6
|
+
import type { I18n, InlineToolbar } from '../../../types/api';
|
|
7
|
+
import type { MenuConfig } from '../../../types/tools';
|
|
8
|
+
import { IconMarker } from '../icons';
|
|
9
|
+
import { SelectionUtils } from '../selection/index';
|
|
10
|
+
import { PopoverItemType } from '../utils/popover';
|
|
11
|
+
import { isMarkTag, findMarkElement } from './utils/marker-dom-utils';
|
|
12
|
+
import {
|
|
13
|
+
isRangeFormatted,
|
|
14
|
+
collectFormattingAncestors,
|
|
15
|
+
} from './utils/formatting-range-utils';
|
|
16
|
+
import { createColorPicker } from '../shared/color-picker';
|
|
17
|
+
import type { ColorPickerHandle } from '../shared/color-picker';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Color mode type — either text color or background color
|
|
21
|
+
*/
|
|
22
|
+
type ColorMode = 'color' | 'background-color';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The opposite color mode key for checking remaining styles
|
|
26
|
+
*/
|
|
27
|
+
const OPPOSITE_MODE: Record<ColorMode, ColorMode> = {
|
|
28
|
+
'color': 'background-color',
|
|
29
|
+
'background-color': 'color',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Marker Color Inline Tool
|
|
34
|
+
*
|
|
35
|
+
* Wraps selected text in <mark> with color or background-color styles.
|
|
36
|
+
*/
|
|
37
|
+
export class MarkerInlineTool implements InlineTool {
|
|
38
|
+
/**
|
|
39
|
+
* Specifies Tool as Inline Toolbar Tool
|
|
40
|
+
*/
|
|
41
|
+
public static isInline = true;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Title for the Inline Tool
|
|
45
|
+
*/
|
|
46
|
+
public static title = 'Color';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Translation key for i18n
|
|
50
|
+
*/
|
|
51
|
+
public static titleKey = 'marker';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Keyboard shortcut to open the marker color picker
|
|
55
|
+
*/
|
|
56
|
+
public static shortcut = 'CMD+SHIFT+H';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* CSS properties allowed on <mark> elements.
|
|
60
|
+
* All other properties are stripped during sanitization to prevent
|
|
61
|
+
* style-based attacks (e.g. position:fixed overlays via pasted HTML).
|
|
62
|
+
*/
|
|
63
|
+
private static readonly ALLOWED_STYLE_PROPS = new Set(['color', 'background-color']);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sanitizer Rule — preserve <mark> tags with only color-related style properties.
|
|
67
|
+
*
|
|
68
|
+
* Uses a function-based rule so HTMLJanitor calls it with the live DOM node,
|
|
69
|
+
* allowing in-place filtering of CSS properties before the node is serialized.
|
|
70
|
+
*/
|
|
71
|
+
public static get sanitize(): SanitizerConfig {
|
|
72
|
+
return {
|
|
73
|
+
mark: (node: Element): { [attr: string]: boolean | string } => {
|
|
74
|
+
const el = node as HTMLElement;
|
|
75
|
+
const style = el.style;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Collect property names first, then remove disallowed ones.
|
|
79
|
+
* This avoids mutating the CSSStyleDeclaration while iterating its indices.
|
|
80
|
+
*/
|
|
81
|
+
const props = Array.from({ length: style.length }, (_, i) => style.item(i));
|
|
82
|
+
|
|
83
|
+
for (const prop of props) {
|
|
84
|
+
if (!MarkerInlineTool.ALLOWED_STYLE_PROPS.has(prop)) {
|
|
85
|
+
style.removeProperty(prop);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return style.length > 0 ? { style: true } : {};
|
|
90
|
+
},
|
|
91
|
+
} as SanitizerConfig;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* I18n API
|
|
96
|
+
*/
|
|
97
|
+
private i18n: I18n;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Inline toolbar API
|
|
101
|
+
*/
|
|
102
|
+
private inlineToolbar: InlineToolbar;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* SelectionUtils instance for saving/restoring selection
|
|
106
|
+
*/
|
|
107
|
+
private selection: SelectionUtils;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Currently active color mode, updated by the shared picker via callback
|
|
111
|
+
*/
|
|
112
|
+
private colorMode: ColorMode = 'color';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The color picker handle with element and control methods
|
|
116
|
+
*/
|
|
117
|
+
private picker: ColorPickerHandle;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param options - Inline tool constructor options with API
|
|
121
|
+
*/
|
|
122
|
+
constructor({ api }: InlineToolConstructorOptions) {
|
|
123
|
+
this.i18n = api.i18n;
|
|
124
|
+
this.inlineToolbar = api.inlineToolbar;
|
|
125
|
+
this.selection = new SelectionUtils();
|
|
126
|
+
|
|
127
|
+
this.picker = createColorPicker({
|
|
128
|
+
i18n: this.i18n,
|
|
129
|
+
testIdPrefix: 'marker',
|
|
130
|
+
defaultModeIndex: 0,
|
|
131
|
+
modes: [
|
|
132
|
+
{ key: 'color', labelKey: 'tools.marker.textColor', presetField: 'text' },
|
|
133
|
+
{ key: 'background-color', labelKey: 'tools.marker.background', presetField: 'bg' },
|
|
134
|
+
],
|
|
135
|
+
onColorSelect: (color, modeKey) => {
|
|
136
|
+
this.colorMode = modeKey as ColorMode;
|
|
137
|
+
|
|
138
|
+
if (color !== null) {
|
|
139
|
+
this.applyColor(this.colorMode, color);
|
|
140
|
+
} else {
|
|
141
|
+
this.removeColor(this.colorMode);
|
|
142
|
+
}
|
|
143
|
+
this.selection.setFakeBackground();
|
|
144
|
+
this.selection.save();
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create button for Inline Toolbar
|
|
151
|
+
*/
|
|
152
|
+
public render(): MenuConfig {
|
|
153
|
+
return {
|
|
154
|
+
icon: IconMarker,
|
|
155
|
+
name: 'marker',
|
|
156
|
+
isActive: () => {
|
|
157
|
+
const selection = window.getSelection();
|
|
158
|
+
|
|
159
|
+
if (!selection || selection.rangeCount === 0) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const range = selection.getRangeAt(0);
|
|
164
|
+
|
|
165
|
+
return isRangeFormatted(range, isMarkTag, { ignoreWhitespace: true });
|
|
166
|
+
},
|
|
167
|
+
children: {
|
|
168
|
+
hideChevron: true,
|
|
169
|
+
width: '200px',
|
|
170
|
+
items: [
|
|
171
|
+
{
|
|
172
|
+
type: PopoverItemType.Html,
|
|
173
|
+
element: this.picker.element,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
onOpen: () => {
|
|
177
|
+
this.onPickerOpen();
|
|
178
|
+
},
|
|
179
|
+
onClose: () => {
|
|
180
|
+
this.onPickerClose();
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Apply a color to the current selection by wrapping in <mark>
|
|
188
|
+
* @param mode - 'color' or 'background-color'
|
|
189
|
+
* @param value - CSS color value
|
|
190
|
+
*/
|
|
191
|
+
public applyColor(mode: ColorMode, value: string): void {
|
|
192
|
+
this.restoreSelectionIfSaved();
|
|
193
|
+
|
|
194
|
+
const selection = window.getSelection();
|
|
195
|
+
|
|
196
|
+
if (!selection || selection.rangeCount === 0) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const range = selection.getRangeAt(0);
|
|
201
|
+
|
|
202
|
+
if (range.collapsed) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if the entire selection is already inside a single mark element
|
|
208
|
+
*/
|
|
209
|
+
const existingMark = this.findContainingMark(range);
|
|
210
|
+
|
|
211
|
+
if (existingMark) {
|
|
212
|
+
/**
|
|
213
|
+
* If the selection covers the entire mark content, update in-place
|
|
214
|
+
*/
|
|
215
|
+
const markRange = document.createRange();
|
|
216
|
+
|
|
217
|
+
markRange.selectNodeContents(existingMark);
|
|
218
|
+
|
|
219
|
+
const coversAll =
|
|
220
|
+
range.compareBoundaryPoints(Range.START_TO_START, markRange) <= 0 &&
|
|
221
|
+
range.compareBoundaryPoints(Range.END_TO_END, markRange) >= 0;
|
|
222
|
+
|
|
223
|
+
if (coversAll) {
|
|
224
|
+
existingMark.style.setProperty(mode, value);
|
|
225
|
+
this.ensureTransparentBg(existingMark);
|
|
226
|
+
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Partial selection: split the mark around the selection
|
|
232
|
+
* so the new color applies only to the selected text
|
|
233
|
+
*/
|
|
234
|
+
this.splitMarkAroundRange(existingMark, range, mode, value);
|
|
235
|
+
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Split any marks that extend beyond the selection boundaries
|
|
241
|
+
* so removeNestedMarkStyle only processes the portion within the range
|
|
242
|
+
*/
|
|
243
|
+
this.splitMarksAtBoundaries(range);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Remove any nested marks with the same mode before wrapping
|
|
247
|
+
*/
|
|
248
|
+
this.removeNestedMarkStyle(range, mode);
|
|
249
|
+
|
|
250
|
+
const mark = document.createElement('mark');
|
|
251
|
+
|
|
252
|
+
mark.style.setProperty(mode, value);
|
|
253
|
+
this.ensureTransparentBg(mark);
|
|
254
|
+
mark.appendChild(range.extractContents());
|
|
255
|
+
range.insertNode(mark);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Select the newly inserted mark contents
|
|
259
|
+
*/
|
|
260
|
+
selection.removeAllRanges();
|
|
261
|
+
const newRange = document.createRange();
|
|
262
|
+
|
|
263
|
+
newRange.selectNodeContents(mark);
|
|
264
|
+
selection.addRange(newRange);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Remove a color style from the current selection's mark elements
|
|
269
|
+
* @param mode - 'color' or 'background-color'
|
|
270
|
+
*/
|
|
271
|
+
public removeColor(mode: ColorMode): void {
|
|
272
|
+
this.restoreSelectionIfSaved();
|
|
273
|
+
|
|
274
|
+
const selection = window.getSelection();
|
|
275
|
+
|
|
276
|
+
if (!selection || selection.rangeCount === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const range = selection.getRangeAt(0);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Capture range anchors before DOM mutations so we can restore the selection
|
|
284
|
+
* after marks are unwrapped (browsers may collapse selection on DOM changes)
|
|
285
|
+
*/
|
|
286
|
+
const startContainer = range.startContainer;
|
|
287
|
+
const startOffset = range.startOffset;
|
|
288
|
+
const endContainer = range.endContainer;
|
|
289
|
+
const endOffset = range.endOffset;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Also capture the selected text and a surviving parent node so we
|
|
293
|
+
* can fall back to offset-based restoration when anchors become stale.
|
|
294
|
+
* The commonAncestorContainer may itself be the mark element being removed,
|
|
295
|
+
* so walk up to find a parent that will survive the unwrap.
|
|
296
|
+
*/
|
|
297
|
+
const selectedText = range.toString();
|
|
298
|
+
const ancestor = range.commonAncestorContainer;
|
|
299
|
+
const ancestorEl = ancestor.nodeType === Node.ELEMENT_NODE
|
|
300
|
+
? ancestor as HTMLElement
|
|
301
|
+
: ancestor.parentElement;
|
|
302
|
+
const survivingParent = ancestorEl?.closest('mark')
|
|
303
|
+
? ancestorEl.closest('mark')?.parentElement ?? ancestorEl
|
|
304
|
+
: ancestorEl;
|
|
305
|
+
|
|
306
|
+
const markAncestors = collectFormattingAncestors(range, isMarkTag);
|
|
307
|
+
|
|
308
|
+
for (const mark of markAncestors) {
|
|
309
|
+
mark.style.removeProperty(mode);
|
|
310
|
+
|
|
311
|
+
const oppositeMode = OPPOSITE_MODE[mode];
|
|
312
|
+
const oppositeValue = mark.style.getPropertyValue(oppositeMode);
|
|
313
|
+
const hasOtherStyle = oppositeValue !== '' && oppositeValue !== 'transparent';
|
|
314
|
+
|
|
315
|
+
if (!hasOtherStyle) {
|
|
316
|
+
this.unwrapElement(mark);
|
|
317
|
+
} else {
|
|
318
|
+
this.ensureTransparentBg(mark);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Re-establish the selection after DOM mutations.
|
|
324
|
+
* When the range was anchored to text nodes (moved, not cloned by unwrapElement),
|
|
325
|
+
* the original anchors remain valid. When the range was anchored to the mark
|
|
326
|
+
* element itself (e.g. via selectNodeContents), the node is now detached.
|
|
327
|
+
* Check connectivity before attempting restoration; fall back to text-offset
|
|
328
|
+
* restoration when anchors are stale.
|
|
329
|
+
*/
|
|
330
|
+
const startConnected = startContainer.isConnected;
|
|
331
|
+
const endConnected = endContainer.isConnected;
|
|
332
|
+
|
|
333
|
+
if (startConnected && endConnected) {
|
|
334
|
+
try {
|
|
335
|
+
const restoredRange = document.createRange();
|
|
336
|
+
|
|
337
|
+
restoredRange.setStart(startContainer, startOffset);
|
|
338
|
+
restoredRange.setEnd(endContainer, endOffset);
|
|
339
|
+
selection.removeAllRanges();
|
|
340
|
+
selection.addRange(restoredRange);
|
|
341
|
+
} catch {
|
|
342
|
+
this.restoreSelectionByText(selection, survivingParent, selectedText);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
this.restoreSelectionByText(selection, survivingParent, selectedText);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Called when the picker popover opens — save selection, reset tab state,
|
|
351
|
+
* and detect the current selection's color to highlight the active swatch.
|
|
352
|
+
*/
|
|
353
|
+
private onPickerOpen(): void {
|
|
354
|
+
this.picker.reset();
|
|
355
|
+
|
|
356
|
+
const activeColor = this.detectSelectionColor();
|
|
357
|
+
|
|
358
|
+
if (activeColor) {
|
|
359
|
+
this.picker.setActiveColor(activeColor.value, activeColor.mode);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.selection.setFakeBackground();
|
|
363
|
+
this.selection.save();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Called when the picker popover closes — clean up selection state
|
|
368
|
+
*/
|
|
369
|
+
private onPickerClose(): void {
|
|
370
|
+
this.selection.removeFakeBackground();
|
|
371
|
+
|
|
372
|
+
if (this.selection.savedSelectionRange) {
|
|
373
|
+
this.selection.restore();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.selection.clearSaved();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Detect the color of the current selection's mark element.
|
|
381
|
+
* Returns the first color mode found (text color preferred over background).
|
|
382
|
+
*/
|
|
383
|
+
private detectSelectionColor(): { value: string; mode: string } | null {
|
|
384
|
+
const selection = window.getSelection();
|
|
385
|
+
|
|
386
|
+
if (!selection || selection.rangeCount === 0) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const range = selection.getRangeAt(0);
|
|
391
|
+
const mark = findMarkElement(range.startContainer);
|
|
392
|
+
|
|
393
|
+
if (!mark) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const textColor = mark.style.getPropertyValue('color');
|
|
398
|
+
|
|
399
|
+
if (textColor && textColor !== 'transparent') {
|
|
400
|
+
return { value: textColor, mode: 'color' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const bgColor = mark.style.getPropertyValue('background-color');
|
|
404
|
+
|
|
405
|
+
if (bgColor && bgColor !== 'transparent') {
|
|
406
|
+
return { value: bgColor, mode: 'background-color' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Restore selection from saved state if available
|
|
414
|
+
*/
|
|
415
|
+
private restoreSelectionIfSaved(): void {
|
|
416
|
+
if (this.selection.savedSelectionRange) {
|
|
417
|
+
this.selection.removeFakeBackground();
|
|
418
|
+
this.selection.restore();
|
|
419
|
+
this.selection.clearSaved();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Find a single mark element that fully contains the range
|
|
425
|
+
* @param range - The range to check
|
|
426
|
+
*/
|
|
427
|
+
private findContainingMark(range: Range): HTMLElement | null {
|
|
428
|
+
const startMark = findMarkElement(range.startContainer);
|
|
429
|
+
const endMark = findMarkElement(range.endContainer);
|
|
430
|
+
|
|
431
|
+
if (startMark && startMark === endMark) {
|
|
432
|
+
return startMark;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Remove a specific style property from nested mark elements within a range
|
|
440
|
+
* @param range - The range to process
|
|
441
|
+
* @param mode - The style property to remove
|
|
442
|
+
*/
|
|
443
|
+
private removeNestedMarkStyle(range: Range, mode: ColorMode): void {
|
|
444
|
+
const liveMarks = collectFormattingAncestors(range, isMarkTag);
|
|
445
|
+
|
|
446
|
+
for (const mark of liveMarks) {
|
|
447
|
+
mark.style.removeProperty(mode);
|
|
448
|
+
|
|
449
|
+
const oppositeMode = OPPOSITE_MODE[mode];
|
|
450
|
+
const oppositeValue = mark.style.getPropertyValue(oppositeMode);
|
|
451
|
+
const hasOtherStyle = oppositeValue !== '' && oppositeValue !== 'transparent';
|
|
452
|
+
|
|
453
|
+
if (!hasOtherStyle) {
|
|
454
|
+
this.unwrapElement(mark);
|
|
455
|
+
} else {
|
|
456
|
+
this.ensureTransparentBg(mark);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Split a mark element around a range so only the selected portion gets the new style.
|
|
463
|
+
* Produces up to three segments: before (original style), selected (new style), after (original style).
|
|
464
|
+
* @param mark - The existing mark element to split
|
|
465
|
+
* @param range - The selection range within the mark
|
|
466
|
+
* @param mode - The style property to set on the selected portion
|
|
467
|
+
* @param value - The CSS value for the style property
|
|
468
|
+
*/
|
|
469
|
+
private splitMarkAroundRange(mark: HTMLElement, range: Range, mode: ColorMode, value: string): void {
|
|
470
|
+
const parent = mark.parentNode;
|
|
471
|
+
|
|
472
|
+
if (!parent) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const beforeRange = document.createRange();
|
|
477
|
+
|
|
478
|
+
beforeRange.setStart(mark, 0);
|
|
479
|
+
beforeRange.setEnd(range.startContainer, range.startOffset);
|
|
480
|
+
|
|
481
|
+
const afterRange = document.createRange();
|
|
482
|
+
|
|
483
|
+
afterRange.setStart(range.endContainer, range.endOffset);
|
|
484
|
+
afterRange.setEnd(mark, mark.childNodes.length);
|
|
485
|
+
|
|
486
|
+
const beforeContents = beforeRange.extractContents();
|
|
487
|
+
const selectedContents = range.extractContents();
|
|
488
|
+
const afterContents = afterRange.extractContents();
|
|
489
|
+
|
|
490
|
+
const newMark = document.createElement('mark');
|
|
491
|
+
|
|
492
|
+
newMark.style.cssText = mark.style.cssText;
|
|
493
|
+
newMark.style.setProperty(mode, value);
|
|
494
|
+
this.ensureTransparentBg(newMark);
|
|
495
|
+
newMark.appendChild(selectedContents);
|
|
496
|
+
|
|
497
|
+
const fragment = document.createDocumentFragment();
|
|
498
|
+
|
|
499
|
+
if (beforeContents.textContent) {
|
|
500
|
+
const beforeMark = document.createElement('mark');
|
|
501
|
+
|
|
502
|
+
beforeMark.style.cssText = mark.style.cssText;
|
|
503
|
+
this.ensureTransparentBg(beforeMark);
|
|
504
|
+
beforeMark.appendChild(beforeContents);
|
|
505
|
+
fragment.appendChild(beforeMark);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fragment.appendChild(newMark);
|
|
509
|
+
|
|
510
|
+
if (afterContents.textContent) {
|
|
511
|
+
const afterMark = document.createElement('mark');
|
|
512
|
+
|
|
513
|
+
afterMark.style.cssText = mark.style.cssText;
|
|
514
|
+
this.ensureTransparentBg(afterMark);
|
|
515
|
+
afterMark.appendChild(afterContents);
|
|
516
|
+
fragment.appendChild(afterMark);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
parent.replaceChild(fragment, mark);
|
|
520
|
+
|
|
521
|
+
const selection = window.getSelection();
|
|
522
|
+
|
|
523
|
+
if (selection) {
|
|
524
|
+
selection.removeAllRanges();
|
|
525
|
+
|
|
526
|
+
const newRange = document.createRange();
|
|
527
|
+
|
|
528
|
+
newRange.selectNodeContents(newMark);
|
|
529
|
+
selection.addRange(newRange);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Split mark elements at range boundaries so that marks extending
|
|
535
|
+
* beyond the selection are separated into inside/outside portions.
|
|
536
|
+
* This preserves mark styling on text outside the selection range.
|
|
537
|
+
* @param range - The selection range
|
|
538
|
+
*/
|
|
539
|
+
private splitMarksAtBoundaries(range: Range): void {
|
|
540
|
+
const marks = collectFormattingAncestors(range, isMarkTag);
|
|
541
|
+
|
|
542
|
+
for (const mark of marks) {
|
|
543
|
+
const markRange = document.createRange();
|
|
544
|
+
|
|
545
|
+
markRange.selectNodeContents(mark);
|
|
546
|
+
|
|
547
|
+
const rangeStartsBeforeMark = range.compareBoundaryPoints(Range.START_TO_START, markRange) <= 0;
|
|
548
|
+
const rangeEndsAfterMark = range.compareBoundaryPoints(Range.END_TO_END, markRange) >= 0;
|
|
549
|
+
|
|
550
|
+
if (rangeStartsBeforeMark && rangeEndsAfterMark) {
|
|
551
|
+
/**
|
|
552
|
+
* Range fully contains the mark — no split needed
|
|
553
|
+
*/
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!mark.parentNode) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Split at the end boundary first (to avoid invalidating start offsets)
|
|
563
|
+
*/
|
|
564
|
+
if (!rangeEndsAfterMark) {
|
|
565
|
+
this.extractTrailingMark(mark, range.endContainer, range.endOffset);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Split at the start boundary
|
|
570
|
+
*/
|
|
571
|
+
if (!rangeStartsBeforeMark) {
|
|
572
|
+
this.extractLeadingMark(mark, range.startContainer, range.startOffset);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Extract content after a boundary point from a mark into a new sibling mark.
|
|
579
|
+
* @param mark - The mark to split
|
|
580
|
+
* @param boundaryNode - The node at the boundary
|
|
581
|
+
* @param boundaryOffset - The offset at the boundary
|
|
582
|
+
*/
|
|
583
|
+
private extractTrailingMark(mark: HTMLElement, boundaryNode: Node, boundaryOffset: number): void {
|
|
584
|
+
const trailingRange = document.createRange();
|
|
585
|
+
|
|
586
|
+
trailingRange.setStart(boundaryNode, boundaryOffset);
|
|
587
|
+
trailingRange.setEnd(mark, mark.childNodes.length);
|
|
588
|
+
|
|
589
|
+
const contents = trailingRange.extractContents();
|
|
590
|
+
|
|
591
|
+
if (!contents.textContent) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const trailingMark = document.createElement('mark');
|
|
596
|
+
|
|
597
|
+
trailingMark.style.cssText = mark.style.cssText;
|
|
598
|
+
trailingMark.appendChild(contents);
|
|
599
|
+
mark.after(trailingMark);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Extract content before a boundary point from a mark into a new sibling mark.
|
|
604
|
+
* @param mark - The mark to split
|
|
605
|
+
* @param boundaryNode - The node at the boundary
|
|
606
|
+
* @param boundaryOffset - The offset at the boundary
|
|
607
|
+
*/
|
|
608
|
+
private extractLeadingMark(mark: HTMLElement, boundaryNode: Node, boundaryOffset: number): void {
|
|
609
|
+
const leadingRange = document.createRange();
|
|
610
|
+
|
|
611
|
+
leadingRange.setStart(mark, 0);
|
|
612
|
+
leadingRange.setEnd(boundaryNode, boundaryOffset);
|
|
613
|
+
|
|
614
|
+
const contents = leadingRange.extractContents();
|
|
615
|
+
|
|
616
|
+
if (!contents.textContent) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const leadingMark = document.createElement('mark');
|
|
621
|
+
|
|
622
|
+
leadingMark.style.cssText = mark.style.cssText;
|
|
623
|
+
leadingMark.appendChild(contents);
|
|
624
|
+
mark.before(leadingMark);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Restore selection by finding the selected text within a surviving parent.
|
|
629
|
+
* Used as a fallback when range anchors become stale after DOM mutations.
|
|
630
|
+
* @param selection - The window selection to restore
|
|
631
|
+
* @param parent - A parent element that survived the DOM mutation
|
|
632
|
+
* @param text - The text content that was selected before mutation
|
|
633
|
+
*/
|
|
634
|
+
private restoreSelectionByText(
|
|
635
|
+
selection: Selection,
|
|
636
|
+
parent: HTMLElement | null,
|
|
637
|
+
text: string
|
|
638
|
+
): void {
|
|
639
|
+
if (!parent || text.length === 0) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const fullText = parent.textContent ?? '';
|
|
644
|
+
const startIdx = fullText.indexOf(text);
|
|
645
|
+
|
|
646
|
+
if (startIdx === -1) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const endIdx = startIdx + text.length;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Walk text nodes to find the nodes and offsets corresponding
|
|
654
|
+
* to the character positions in the parent's textContent
|
|
655
|
+
*/
|
|
656
|
+
const { startNode, startNodeOffset, endNode, endNodeOffset } = this.findTextBoundaries(parent, startIdx, endIdx);
|
|
657
|
+
|
|
658
|
+
if (startNode && endNode) {
|
|
659
|
+
const restoredRange = document.createRange();
|
|
660
|
+
|
|
661
|
+
restoredRange.setStart(startNode, startNodeOffset);
|
|
662
|
+
restoredRange.setEnd(endNode, endNodeOffset);
|
|
663
|
+
selection.removeAllRanges();
|
|
664
|
+
selection.addRange(restoredRange);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Walk text nodes within a parent to find the nodes and offsets
|
|
670
|
+
* corresponding to character positions in the parent's textContent.
|
|
671
|
+
* @param parent - The parent element to walk
|
|
672
|
+
* @param startIdx - The start character index
|
|
673
|
+
* @param endIdx - The end character index
|
|
674
|
+
* @returns An object with the start/end nodes and their offsets
|
|
675
|
+
*/
|
|
676
|
+
private findTextBoundaries(
|
|
677
|
+
parent: HTMLElement,
|
|
678
|
+
startIdx: number,
|
|
679
|
+
endIdx: number
|
|
680
|
+
): { startNode: Text | null; startNodeOffset: number; endNode: Text | null; endNodeOffset: number } {
|
|
681
|
+
const walker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
|
|
682
|
+
const result = { startNode: null as Text | null, startNodeOffset: 0, endNode: null as Text | null, endNodeOffset: 0 };
|
|
683
|
+
const charCounter = { value: 0 };
|
|
684
|
+
|
|
685
|
+
while (walker.nextNode()) {
|
|
686
|
+
const node = walker.currentNode as Text;
|
|
687
|
+
const nodeLength = node.textContent?.length ?? 0;
|
|
688
|
+
|
|
689
|
+
if (result.startNode === null && charCounter.value + nodeLength > startIdx) {
|
|
690
|
+
result.startNode = node;
|
|
691
|
+
result.startNodeOffset = startIdx - charCounter.value;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (charCounter.value + nodeLength >= endIdx) {
|
|
695
|
+
result.endNode = node;
|
|
696
|
+
result.endNodeOffset = endIdx - charCounter.value;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
charCounter.value += nodeLength;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Unwrap an element, moving its children to its parent
|
|
708
|
+
* @param element - Element to unwrap
|
|
709
|
+
*/
|
|
710
|
+
private unwrapElement(element: HTMLElement): void {
|
|
711
|
+
const parent = element.parentNode;
|
|
712
|
+
|
|
713
|
+
if (!parent) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
while (element.firstChild) {
|
|
718
|
+
parent.insertBefore(element.firstChild, element);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
parent.removeChild(element);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Ensure a mark with text color has an explicit transparent background
|
|
726
|
+
* to override the browser's default yellow <mark> background.
|
|
727
|
+
* @param mark - The mark element to check
|
|
728
|
+
*/
|
|
729
|
+
private ensureTransparentBg(mark: HTMLElement): void {
|
|
730
|
+
if (
|
|
731
|
+
mark.style.getPropertyValue('color') &&
|
|
732
|
+
!mark.style.getPropertyValue('background-color')
|
|
733
|
+
) {
|
|
734
|
+
mark.style.setProperty('background-color', 'transparent');
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|