@jackuait/blok 0.7.3 → 0.8.0

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.
Files changed (228) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-BmlbETK7.mjs → blok-CdiMFzDz.mjs} +2143 -2134
  3. package/dist/chunks/{constants-WhLyFkza.mjs → constants-CjWRVOdm.mjs} +84 -84
  4. package/dist/chunks/{i18next-loader-CZARkla1.mjs → i18next-loader-p-7ioTwr.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-BQa0F2X6.mjs → lightweight-i18n-BPeH69Dl.mjs} +5 -1
  6. package/dist/{messages-BbLCMWln2.mjs → chunks/messages--aM83pib2.mjs} +14 -1
  7. package/dist/chunks/{messages-ni0ahgYk2.mjs → messages-4OvVdaG52.mjs} +14 -1
  8. package/dist/chunks/{messages-DnPkoMz1.mjs → messages-5ohIWynJ.mjs} +14 -1
  9. package/dist/{messages-CzSLUJQt.mjs → chunks/messages-5thhSeME.mjs} +14 -1
  10. package/dist/chunks/{messages-hngFJrES2.mjs → messages-8pf7gRQm2.mjs} +14 -1
  11. package/dist/chunks/{messages-DcPtg90i.mjs → messages-B-i-d1Bc.mjs} +14 -1
  12. package/dist/chunks/{messages-CRxkRJRN.mjs → messages-B9zocutJ.mjs} +14 -1
  13. package/dist/chunks/{messages-BUy3odZo.mjs → messages-BAC7nLeM.mjs} +14 -1
  14. package/dist/chunks/{messages-DjOY_EqX.mjs → messages-BARPMN7R.mjs} +14 -1
  15. package/dist/{messages-Bsz7Qgj_.mjs → chunks/messages-BAgTIgPH.mjs} +15 -2
  16. package/dist/{messages-C7R0m6oE2.mjs → chunks/messages-BDTgiBJY2.mjs} +14 -1
  17. package/dist/chunks/{messages-BM1Su_Uy2.mjs → messages-BHf_VcXb2.mjs} +14 -1
  18. package/dist/{messages-wRvz0vQ3.mjs → chunks/messages-BIrkzbBP.mjs} +14 -1
  19. package/dist/chunks/{messages-DotEkUpQ.mjs → messages-BKjLgO5d.mjs} +14 -1
  20. package/dist/{messages-gldjQk7M.mjs → chunks/messages-BMzmli1K.mjs} +14 -1
  21. package/dist/chunks/{messages-CficsmSH2.mjs → messages-BO_jtRbZ2.mjs} +14 -1
  22. package/dist/{messages-B2bHgIcC2.mjs → chunks/messages-B_fFjpX12.mjs} +14 -1
  23. package/dist/chunks/{messages-CnzaTbel.mjs → messages-Bdk5tBNu.mjs} +14 -1
  24. package/dist/{messages-CRMZ79Xf.mjs → chunks/messages-BgaGPFuy2.mjs} +14 -1
  25. package/dist/{messages-DCOKudVN.mjs → chunks/messages-BsYzg2le.mjs} +14 -1
  26. package/dist/chunks/{messages-Dxrg70jo.mjs → messages-BtS6JWMT.mjs} +14 -1
  27. package/dist/{messages-Cgy54529.mjs → chunks/messages-BvlSf_pu.mjs} +14 -1
  28. package/dist/{messages-oH0ADQ362.mjs → chunks/messages-C03I_oR-2.mjs} +14 -1
  29. package/dist/chunks/{messages-B_nVGWdk.mjs → messages-CDJLoStb.mjs} +14 -1
  30. package/dist/chunks/{messages-ClOxDE0y2.mjs → messages-CH_cexo62.mjs} +14 -1
  31. package/dist/chunks/{messages-DoDbCS02.mjs → messages-CIBuZccC.mjs} +14 -1
  32. package/dist/{messages-CkmVEyEQ2.mjs → chunks/messages-CIZkNCpW.mjs} +14 -1
  33. package/dist/{messages-CJYE0_hr2.mjs → chunks/messages-CIugNDDO2.mjs} +14 -1
  34. package/dist/{messages-DpY9s4Qi2.mjs → chunks/messages-COkQfKa72.mjs} +14 -1
  35. package/dist/{messages-Bw-GC0m5.mjs → chunks/messages-CSpfBhlK2.mjs} +14 -1
  36. package/dist/chunks/{messages-tBHnC2Rj2.mjs → messages-CWp6-Y8f2.mjs} +14 -1
  37. package/dist/{messages-GrVSCmXW.mjs → chunks/messages-Co4WFeQ8.mjs} +14 -1
  38. package/dist/{messages-voUPclMU.mjs → chunks/messages-CoRQE_Jc.mjs} +14 -1
  39. package/dist/{messages-DGZQXeav2.mjs → chunks/messages-CuRN1_ep2.mjs} +14 -1
  40. package/dist/{messages-BwdowdYD.mjs → chunks/messages-D0IKicWg.mjs} +14 -1
  41. package/dist/chunks/{messages-1C3OS98e.mjs → messages-D14soBOO.mjs} +14 -1
  42. package/dist/chunks/{messages-nE9Ko73n2.mjs → messages-D1SqLcxI2.mjs} +14 -1
  43. package/dist/{messages-COU4L-pL2.mjs → chunks/messages-D2uBlGXR2.mjs} +14 -1
  44. package/dist/{messages-m1uf_AMy2.mjs → chunks/messages-DDpgr0B1.mjs} +14 -1
  45. package/dist/{messages-CB0RKGVM.mjs → chunks/messages-DEGzGmEQ2.mjs} +14 -1
  46. package/dist/{messages--XEfVx572.mjs → chunks/messages-DKChC8Qu.mjs} +14 -1
  47. package/dist/{messages-BP8ZuVaD.mjs → chunks/messages-DKCoHa5D.mjs} +14 -1
  48. package/dist/chunks/{messages-C3AJz_i6.mjs → messages-DPoPrTiZ.mjs} +14 -1
  49. package/dist/{messages-CaJRIGUu2.mjs → chunks/messages-DQOk-dTH.mjs} +14 -1
  50. package/dist/chunks/{messages-uLIUXFmU.mjs → messages-DUp8NnKT.mjs} +15 -2
  51. package/dist/chunks/{messages-rJdSnvyi.mjs → messages-DVg69mRj.mjs} +14 -1
  52. package/dist/chunks/{messages-Cq6wj6FG.mjs → messages-DeLzropc.mjs} +14 -1
  53. package/dist/{messages-BlnZ8CkJ2.mjs → chunks/messages-Df69rfTF2.mjs} +15 -2
  54. package/dist/{messages-BZgGD0zf2.mjs → chunks/messages-DfsHFEWa.mjs} +14 -1
  55. package/dist/chunks/{messages-yJmwc3zD.mjs → messages-DgZnRQRS.mjs} +14 -1
  56. package/dist/chunks/{messages-m6bLP64R2.mjs → messages-DhGHk-Ma2.mjs} +14 -1
  57. package/dist/{messages-3aRjZXpv.mjs → chunks/messages-Di7mfvB8.mjs} +14 -1
  58. package/dist/chunks/{messages-C1iQkKu82.mjs → messages-DlM9TmqS2.mjs} +14 -1
  59. package/dist/chunks/{messages-CejEH4FW2.mjs → messages-DmrsEYQm2.mjs} +14 -1
  60. package/dist/chunks/{messages-DK6pBwD2.mjs → messages-Dr-Ig3sw.mjs} +14 -1
  61. package/dist/{messages-BOxe7ewT.mjs → chunks/messages-DrXNb1gu.mjs} +14 -1
  62. package/dist/chunks/{messages-CvLXClh9.mjs → messages-EDTq4Q52.mjs} +14 -1
  63. package/dist/{messages-huTzItxA.mjs → chunks/messages-Ehx9YYeb2.mjs} +14 -1
  64. package/dist/chunks/{messages-CxTq0x772.mjs → messages-FCmAVA792.mjs} +14 -1
  65. package/dist/{messages-q7HzQPVt.mjs → chunks/messages-LL3Tflph.mjs} +14 -1
  66. package/dist/chunks/{messages-D9qyilS0.mjs → messages-N72K1hw3.mjs} +14 -1
  67. package/dist/chunks/{messages-C9XSSqS5.mjs → messages-Nz8C7Znm.mjs} +15 -2
  68. package/dist/chunks/{messages-D6SAC8Lc2.mjs → messages-OIelQDL32.mjs} +14 -1
  69. package/dist/{messages-BQJzUYP-.mjs → chunks/messages-SsrFJhTN.mjs} +14 -1
  70. package/dist/{messages-CVBsztOg.mjs → chunks/messages-ohtcmr1w.mjs} +14 -1
  71. package/dist/chunks/{messages-CVzvKl6U.mjs → messages-stUQR58d.mjs} +14 -1
  72. package/dist/{messages-DlrZrm3s.mjs → chunks/messages-wUoSWFsJ2.mjs} +14 -1
  73. package/dist/chunks/{tools-BCb5bMO3.mjs → tools-B5yNeTZh.mjs} +1086 -802
  74. package/dist/full.mjs +15 -13
  75. package/dist/locales.mjs +72 -68
  76. package/dist/{messages-mVFAkdcY.mjs → messages-1_FCq0It.mjs} +14 -1
  77. package/dist/{messages-DkkrjINb2.mjs → messages-8zo-T-Nx2.mjs} +14 -1
  78. package/dist/{chunks/messages-DacahKek.mjs → messages-B21zLG6b.mjs} +14 -1
  79. package/dist/{messages-BBgyeB_N.mjs → messages-B9ythxux.mjs} +14 -1
  80. package/dist/{messages-C_RPN2GV.mjs → messages-BAZK-8Zb.mjs} +14 -1
  81. package/dist/{messages-D6Sr5cUE.mjs → messages-BB8umWL1.mjs} +14 -1
  82. package/dist/{chunks/messages-Dhe8_mnQ.mjs → messages-BD_U2EnE.mjs} +14 -1
  83. package/dist/{messages-6G0Eia-2.mjs → messages-BRC9E_sp.mjs} +14 -1
  84. package/dist/{messages-pgqtPci-.mjs → messages-BSLYh59S.mjs} +14 -1
  85. package/dist/{messages-K7ROT6ea.mjs → messages-BSwhWcYw.mjs} +14 -1
  86. package/dist/{messages-BztXgybv2.mjs → messages-BTR3QlIb2.mjs} +14 -1
  87. package/dist/{messages-C0gyqo4h2.mjs → messages-BZXBdD_S2.mjs} +14 -1
  88. package/dist/{chunks/messages-DDRCk44J2.mjs → messages-Bm8I_Li12.mjs} +15 -2
  89. package/dist/{chunks/messages-BKtWlK39.mjs → messages-BrcgNZOJ.mjs} +14 -1
  90. package/dist/{messages-Be1CCcsp2.mjs → messages-BzZ8LahA2.mjs} +14 -1
  91. package/dist/{chunks/messages-B_90PYaG.mjs → messages-C4HpNHfK.mjs} +14 -1
  92. package/dist/{messages-B8M4YRFO2.mjs → messages-C5hD5pSd2.mjs} +14 -1
  93. package/dist/{messages-ruU_e2LK.mjs → messages-C7Rz00Tp.mjs} +14 -1
  94. package/dist/{chunks/messages-C1lqY56F2.mjs → messages-C92tAUYT2.mjs} +14 -1
  95. package/dist/{messages-Kye1BINC.mjs → messages-C9LsEUfG.mjs} +14 -1
  96. package/dist/{chunks/messages-tg78NAmW.mjs → messages-CHWfj4ik.mjs} +14 -1
  97. package/dist/{chunks/messages-DziA-L3p.mjs → messages-CHeucLGl2.mjs} +14 -1
  98. package/dist/{messages-eCyczLYY.mjs → messages-CIxT1nSh.mjs} +14 -1
  99. package/dist/{chunks/messages-Cb-x47kY2.mjs → messages-CKX9iXIb2.mjs} +14 -1
  100. package/dist/{chunks/messages-BjkDJuqh.mjs → messages-CKmmJ9tW.mjs} +14 -1
  101. package/dist/{chunks/messages-Cn5n0nHe2.mjs → messages-CTFwu5-h2.mjs} +14 -1
  102. package/dist/{messages-DAssrN5L2.mjs → messages-CTPFrtK92.mjs} +14 -1
  103. package/dist/{chunks/messages-6EJxSImH.mjs → messages-CWzET_9H2.mjs} +14 -1
  104. package/dist/{chunks/messages-BZ45xBlV.mjs → messages-CkIRmpfZ2.mjs} +14 -1
  105. package/dist/{chunks/messages-kHTrX3wo2.mjs → messages-CmoTIebG2.mjs} +14 -1
  106. package/dist/{messages-DZbsds_k2.mjs → messages-Co26RSCV2.mjs} +14 -1
  107. package/dist/{messages-DrouoDgp.mjs → messages-CqNzlpWi.mjs} +15 -2
  108. package/dist/{chunks/messages-DDr8J4FE.mjs → messages-CrWsU4Xw2.mjs} +14 -1
  109. package/dist/{messages-TseLyyoU.mjs → messages-CsmTziC6.mjs} +14 -1
  110. package/dist/{messages-CAffVeAE2.mjs → messages-CsnglxbV2.mjs} +14 -1
  111. package/dist/{messages-CE305J0p.mjs → messages-Cu7Lr1wp.mjs} +14 -1
  112. package/dist/{chunks/messages-Ul43l29K2.mjs → messages-Cy3Ne_M9.mjs} +14 -1
  113. package/dist/{chunks/messages-CzCqu58X2.mjs → messages-CzZAfGif.mjs} +14 -1
  114. package/dist/{messages-C_4VGaBC.mjs → messages-D5rnT-BC.mjs} +14 -1
  115. package/dist/{messages-MaHxNgKA.mjs → messages-D8iCBMg7.mjs} +14 -1
  116. package/dist/{chunks/messages-BtNOlsMj.mjs → messages-DDJOu049.mjs} +14 -1
  117. package/dist/{messages-BU9luYgO.mjs → messages-DDiP6yex.mjs} +14 -1
  118. package/dist/{messages-DB9U3VIh.mjs → messages-DHJ1fZLL.mjs} +14 -1
  119. package/dist/{chunks/messages-3ePgbbpx2.mjs → messages-DToWAonn2.mjs} +14 -1
  120. package/dist/{messages-BxQ1gzJF2.mjs → messages-DV29fJMD2.mjs} +14 -1
  121. package/dist/{chunks/messages-DJWRON2S.mjs → messages-D_-rh8gl.mjs} +14 -1
  122. package/dist/{messages-e-KHuxtY2.mjs → messages-D_cAZ4Ic2.mjs} +14 -1
  123. package/dist/{chunks/messages-pKUiAqlX2.mjs → messages-Dc7ZzqYN.mjs} +14 -1
  124. package/dist/{chunks/messages-JRavIeeW.mjs → messages-DiSeSE8p.mjs} +14 -1
  125. package/dist/{chunks/messages-DbS9Oibb.mjs → messages-Djhu5RJd.mjs} +14 -1
  126. package/dist/{messages-BdJ1lCo_.mjs → messages-Dr9L1psl.mjs} +15 -2
  127. package/dist/{chunks/messages-IJhiftj5.mjs → messages-EIeWKoc5.mjs} +14 -1
  128. package/dist/{chunks/messages-CteKp81J.mjs → messages-EwoT2jof.mjs} +14 -1
  129. package/dist/{messages-C8iAUPzI.mjs → messages-F7cRf-20.mjs} +14 -1
  130. package/dist/{messages-B9qltgXa.mjs → messages-JZhs_0pf.mjs} +14 -1
  131. package/dist/{chunks/messages-C232njMF2.mjs → messages-JwMkLben.mjs} +14 -1
  132. package/dist/{messages-Cb5JJ8C_2.mjs → messages-LyzjEEIj2.mjs} +14 -1
  133. package/dist/{chunks/messages-DQ4VyVJf2.mjs → messages-SepwOOcg.mjs} +14 -1
  134. package/dist/{chunks/messages-DfTU2I8J.mjs → messages-TI0u6Ked.mjs} +14 -1
  135. package/dist/{chunks/messages-D7aoKTPD.mjs → messages-Tx25QErT.mjs} +14 -1
  136. package/dist/{chunks/messages-D9nReG4C2.mjs → messages-bRqMCja-2.mjs} +14 -1
  137. package/dist/{chunks/messages-h474TGR72.mjs → messages-lEyiemqU2.mjs} +14 -1
  138. package/dist/{messages-BENRci-_2.mjs → messages-mVLfVtQX2.mjs} +14 -1
  139. package/dist/{messages-CEhkWwqI.mjs → messages-ouO9js8Z.mjs} +14 -1
  140. package/dist/{chunks/messages-6z-ULVyk.mjs → messages-ouRGTAKo2.mjs} +14 -1
  141. package/dist/{chunks/messages-DNrK8lCg2.mjs → messages-qV14y_oA2.mjs} +14 -1
  142. package/dist/{chunks/messages-Yk__PXZQ.mjs → messages-rM6YFLZH.mjs} +15 -2
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +3 -3
  145. package/package.json +1 -1
  146. package/src/components/i18n/locales/am/messages.json +14 -1
  147. package/src/components/i18n/locales/ar/messages.json +14 -1
  148. package/src/components/i18n/locales/az/messages.json +14 -1
  149. package/src/components/i18n/locales/bg/messages.json +15 -2
  150. package/src/components/i18n/locales/bn/messages.json +14 -1
  151. package/src/components/i18n/locales/bs/messages.json +14 -1
  152. package/src/components/i18n/locales/cs/messages.json +14 -1
  153. package/src/components/i18n/locales/da/messages.json +14 -1
  154. package/src/components/i18n/locales/de/messages.json +14 -1
  155. package/src/components/i18n/locales/dv/messages.json +14 -1
  156. package/src/components/i18n/locales/el/messages.json +14 -1
  157. package/src/components/i18n/locales/en/messages.json +5 -1
  158. package/src/components/i18n/locales/es/messages.json +14 -1
  159. package/src/components/i18n/locales/et/messages.json +15 -2
  160. package/src/components/i18n/locales/fa/messages.json +14 -1
  161. package/src/components/i18n/locales/fi/messages.json +14 -1
  162. package/src/components/i18n/locales/fil/messages.json +14 -1
  163. package/src/components/i18n/locales/fr/messages.json +14 -1
  164. package/src/components/i18n/locales/gu/messages.json +14 -1
  165. package/src/components/i18n/locales/he/messages.json +14 -1
  166. package/src/components/i18n/locales/hi/messages.json +14 -1
  167. package/src/components/i18n/locales/hr/messages.json +14 -1
  168. package/src/components/i18n/locales/hu/messages.json +14 -1
  169. package/src/components/i18n/locales/hy/messages.json +14 -1
  170. package/src/components/i18n/locales/id/messages.json +14 -1
  171. package/src/components/i18n/locales/it/messages.json +14 -1
  172. package/src/components/i18n/locales/ja/messages.json +14 -1
  173. package/src/components/i18n/locales/ka/messages.json +14 -1
  174. package/src/components/i18n/locales/km/messages.json +14 -1
  175. package/src/components/i18n/locales/kn/messages.json +14 -1
  176. package/src/components/i18n/locales/ko/messages.json +14 -1
  177. package/src/components/i18n/locales/ku/messages.json +14 -1
  178. package/src/components/i18n/locales/lo/messages.json +14 -1
  179. package/src/components/i18n/locales/lt/messages.json +14 -1
  180. package/src/components/i18n/locales/lv/messages.json +14 -1
  181. package/src/components/i18n/locales/mk/messages.json +14 -1
  182. package/src/components/i18n/locales/ml/messages.json +14 -1
  183. package/src/components/i18n/locales/mn/messages.json +14 -1
  184. package/src/components/i18n/locales/mr/messages.json +14 -1
  185. package/src/components/i18n/locales/ms/messages.json +14 -1
  186. package/src/components/i18n/locales/my/messages.json +14 -1
  187. package/src/components/i18n/locales/ne/messages.json +14 -1
  188. package/src/components/i18n/locales/nl/messages.json +14 -1
  189. package/src/components/i18n/locales/no/messages.json +14 -1
  190. package/src/components/i18n/locales/pa/messages.json +14 -1
  191. package/src/components/i18n/locales/pl/messages.json +14 -1
  192. package/src/components/i18n/locales/ps/messages.json +14 -1
  193. package/src/components/i18n/locales/pt/messages.json +14 -1
  194. package/src/components/i18n/locales/ro/messages.json +14 -1
  195. package/src/components/i18n/locales/ru/messages.json +15 -2
  196. package/src/components/i18n/locales/sd/messages.json +14 -1
  197. package/src/components/i18n/locales/si/messages.json +14 -1
  198. package/src/components/i18n/locales/sk/messages.json +14 -1
  199. package/src/components/i18n/locales/sl/messages.json +14 -1
  200. package/src/components/i18n/locales/sq/messages.json +14 -1
  201. package/src/components/i18n/locales/sr/messages.json +14 -1
  202. package/src/components/i18n/locales/sv/messages.json +14 -1
  203. package/src/components/i18n/locales/sw/messages.json +14 -1
  204. package/src/components/i18n/locales/ta/messages.json +14 -1
  205. package/src/components/i18n/locales/te/messages.json +14 -1
  206. package/src/components/i18n/locales/th/messages.json +14 -1
  207. package/src/components/i18n/locales/tr/messages.json +14 -1
  208. package/src/components/i18n/locales/ug/messages.json +14 -1
  209. package/src/components/i18n/locales/uk/messages.json +15 -2
  210. package/src/components/i18n/locales/ur/messages.json +14 -1
  211. package/src/components/i18n/locales/vi/messages.json +14 -1
  212. package/src/components/i18n/locales/yi/messages.json +14 -1
  213. package/src/components/i18n/locales/zh/messages.json +14 -1
  214. package/src/components/icons/index.ts +18 -0
  215. package/src/components/inline-tools/inline-tool-strikethrough.ts +408 -0
  216. package/src/components/inline-tools/inline-tool-underline.ts +408 -0
  217. package/src/components/modules/rectangleSelection.ts +48 -0
  218. package/src/components/modules/toolbar/index.ts +5 -2
  219. package/src/components/shared/color-picker.ts +10 -2
  220. package/src/full.ts +7 -1
  221. package/src/stories/MarkerColors.stories.ts +13 -34
  222. package/src/styles/main.css +1 -1
  223. package/src/tools/header/index.ts +6 -4
  224. package/src/tools/index.ts +4 -0
  225. package/src/tools/list/constants.ts +3 -3
  226. package/src/tools/list/dom-builder.ts +1 -0
  227. package/src/tools/table/table-cell-blocks.ts +25 -0
  228. package/src/tools/toggle/constants.ts +12 -5
@@ -0,0 +1,408 @@
1
+ import type { InlineTool, SanitizerConfig } from '../../../types';
2
+ import type { MenuConfig } from '../../../types/tools';
3
+ import { IconUnderline } from '../icons';
4
+
5
+ import {
6
+ isRangeFormatted,
7
+ findFormattingAncestor,
8
+ hasFormattingAncestor,
9
+ collectFormattingAncestors,
10
+ } from './utils/formatting-range-utils';
11
+
12
+ /**
13
+ * Check if an element is an underline tag (<u>)
14
+ * @param element - The element to check
15
+ */
16
+ const isUnderlineTag = (element: Element): boolean => {
17
+ return element.tagName === 'U';
18
+ };
19
+
20
+ /**
21
+ * Underline Tool
22
+ *
23
+ * Inline Toolbar Tool
24
+ *
25
+ * Style selected text with underline
26
+ */
27
+ export class UnderlineInlineTool implements InlineTool {
28
+ /**
29
+ * Specifies Tool as Inline Toolbar Tool
30
+ * @returns {boolean}
31
+ */
32
+ public static isInline = true;
33
+
34
+ /**
35
+ * Title for the Inline Tool
36
+ */
37
+ public static title = 'Underline';
38
+
39
+ /**
40
+ * Translation key for i18n
41
+ */
42
+ public static titleKey = 'underline';
43
+
44
+ /**
45
+ * Sanitizer Rule
46
+ * Leave <u> tags
47
+ * @returns {object}
48
+ */
49
+ public static get sanitize(): SanitizerConfig {
50
+ return {
51
+ u: {},
52
+ } as SanitizerConfig;
53
+ }
54
+
55
+ /**
56
+ * Create button for Inline Toolbar
57
+ */
58
+ public render(): MenuConfig {
59
+ return {
60
+ icon: IconUnderline,
61
+ name: 'underline',
62
+ onActivate: () => {
63
+ this.toggleUnderline();
64
+ },
65
+ isActive: () => {
66
+ const selection = window.getSelection();
67
+
68
+ return selection ? this.isSelectionVisuallyUnderline(selection) : false;
69
+ },
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Shortcut for underline tool
75
+ */
76
+ public static shortcut = 'CMD+U';
77
+
78
+ /**
79
+ * Apply or remove underline formatting using modern Selection API
80
+ */
81
+ private toggleUnderline(): void {
82
+ const selection = window.getSelection();
83
+
84
+ if (!selection || selection.rangeCount === 0) {
85
+ return;
86
+ }
87
+
88
+ const range = selection.getRangeAt(0);
89
+
90
+ if (range.collapsed) {
91
+ this.toggleCollapsedUnderline(range, selection);
92
+
93
+ return;
94
+ }
95
+
96
+ const shouldUnwrap = this.isRangeUnderline(range, { ignoreWhitespace: true });
97
+
98
+ if (shouldUnwrap) {
99
+ this.unwrapUnderlineTags(range);
100
+ } else {
101
+ this.wrapWithUnderline(range);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Handle toggle for collapsed selection (caret)
107
+ * @param range - Current range
108
+ * @param selection - Current selection
109
+ */
110
+ private toggleCollapsedUnderline(range: Range, selection: Selection): void {
111
+ const isUnderline = this.isRangeUnderline(range, { ignoreWhitespace: true });
112
+
113
+ if (isUnderline) {
114
+ const textNode = document.createTextNode('\u200B');
115
+
116
+ range.insertNode(textNode);
117
+ range.selectNode(textNode);
118
+ this.unwrapUnderlineTags(range);
119
+
120
+ const newRange = document.createRange();
121
+
122
+ newRange.setStart(textNode, 1);
123
+ newRange.setEnd(textNode, 1);
124
+
125
+ selection.removeAllRanges();
126
+ selection.addRange(newRange);
127
+ } else {
128
+ const u = document.createElement('u');
129
+ const textNode = document.createTextNode('\u200B');
130
+
131
+ u.appendChild(textNode);
132
+ range.insertNode(u);
133
+
134
+ const newRange = document.createRange();
135
+
136
+ newRange.setStart(textNode, 1);
137
+ newRange.setEnd(textNode, 1);
138
+
139
+ selection.removeAllRanges();
140
+ selection.addRange(newRange);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check if current selection is within an underline tag
146
+ * @param selection - The Selection object to check
147
+ */
148
+ private isSelectionVisuallyUnderline(selection: Selection): boolean {
149
+ if (selection.rangeCount === 0) {
150
+ return false;
151
+ }
152
+
153
+ const range = selection.getRangeAt(0);
154
+
155
+ return this.isRangeUnderline(range, { ignoreWhitespace: true });
156
+ }
157
+
158
+ /**
159
+ * Check if a range contains underline text
160
+ * @param range - The range to check
161
+ * @param options - Options for checking underline status
162
+ */
163
+ private isRangeUnderline(range: Range, options: { ignoreWhitespace: boolean }): boolean {
164
+ return isRangeFormatted(range, isUnderlineTag, options);
165
+ }
166
+
167
+ /**
168
+ * Wrap selection with <u> tag
169
+ * @param range - The Range object containing the selection to wrap
170
+ */
171
+ private wrapWithUnderline(range: Range): void {
172
+ const html = this.getRangeHtmlWithoutUnderline(range);
173
+ const insertedRange = this.replaceRangeWithHtml(range, `<u>${html}</u>`);
174
+ const selection = window.getSelection();
175
+
176
+ if (selection && insertedRange) {
177
+ selection.removeAllRanges();
178
+ selection.addRange(insertedRange);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Remove underline tags (<u>) while preserving content
184
+ * @param range - The Range object containing the selection to unwrap
185
+ */
186
+ private unwrapUnderlineTags(range: Range): void {
187
+ const underlineAncestors = this.collectUnderlineAncestors(range);
188
+ const selection = window.getSelection();
189
+
190
+ if (!selection) {
191
+ return;
192
+ }
193
+
194
+ const marker = document.createElement('span');
195
+ const fragment = range.extractContents();
196
+
197
+ marker.appendChild(fragment);
198
+ this.removeNestedUnderline(marker);
199
+
200
+ range.insertNode(marker);
201
+
202
+ const markerRange = document.createRange();
203
+
204
+ markerRange.selectNodeContents(marker);
205
+ selection.removeAllRanges();
206
+ selection.addRange(markerRange);
207
+
208
+ for (; ;) {
209
+ const currentUnderline = this.findUnderlineElement(marker);
210
+
211
+ if (!currentUnderline) {
212
+ break;
213
+ }
214
+
215
+ this.moveMarkerOutOfUnderline(marker, currentUnderline);
216
+ }
217
+
218
+ const firstChild = marker.firstChild;
219
+ const lastChild = marker.lastChild;
220
+
221
+ this.unwrapElement(marker);
222
+
223
+ const finalRange = firstChild && lastChild ? (() => {
224
+ const newRange = document.createRange();
225
+
226
+ newRange.setStartBefore(firstChild);
227
+ newRange.setEndAfter(lastChild);
228
+
229
+ selection.removeAllRanges();
230
+ selection.addRange(newRange);
231
+
232
+ return newRange;
233
+ })() : undefined;
234
+
235
+ if (!finalRange) {
236
+ selection.removeAllRanges();
237
+ }
238
+
239
+ underlineAncestors.forEach((element) => {
240
+ if (element.textContent.length === 0) {
241
+ element.remove();
242
+ }
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Check if a node or any of its parents is an underline tag
248
+ * @param node - The node to check
249
+ */
250
+ private hasUnderlineParent(node: Node | null): boolean {
251
+ return hasFormattingAncestor(node, isUnderlineTag);
252
+ }
253
+
254
+ /**
255
+ * Find an underline element in the parent chain
256
+ * @param node - The node to start searching from
257
+ */
258
+ private findUnderlineElement(node: Node | null): HTMLElement | null {
259
+ return findFormattingAncestor(node, isUnderlineTag);
260
+ }
261
+
262
+ /**
263
+ * Collect all underline ancestor elements within a range
264
+ * @param range - The range to search for underline ancestors
265
+ */
266
+ private collectUnderlineAncestors(range: Range): HTMLElement[] {
267
+ return collectFormattingAncestors(range, isUnderlineTag);
268
+ }
269
+
270
+ /**
271
+ * Get HTML content of a range with underline tags removed
272
+ * @param range - The range to extract HTML from
273
+ */
274
+ private getRangeHtmlWithoutUnderline(range: Range): string {
275
+ const contents = range.cloneContents();
276
+
277
+ this.removeNestedUnderline(contents);
278
+
279
+ const container = document.createElement('div');
280
+
281
+ container.appendChild(contents);
282
+
283
+ return container.innerHTML;
284
+ }
285
+
286
+ /**
287
+ * Remove nested underline tags from a root node
288
+ * @param root - The root node to process
289
+ */
290
+ private removeNestedUnderline(root: ParentNode): void {
291
+ const underlineNodes = root.querySelectorAll('u');
292
+
293
+ underlineNodes.forEach((node) => {
294
+ this.unwrapElement(node);
295
+ });
296
+ }
297
+
298
+ /**
299
+ * Unwrap an element by moving its children to the parent
300
+ * @param element - The element to unwrap
301
+ */
302
+ private unwrapElement(element: Element): void {
303
+ const parent = element.parentNode;
304
+
305
+ if (!parent) {
306
+ element.remove();
307
+
308
+ return;
309
+ }
310
+
311
+ while (element.firstChild) {
312
+ parent.insertBefore(element.firstChild, element);
313
+ }
314
+
315
+ parent.removeChild(element);
316
+ }
317
+
318
+ /**
319
+ * Replace the current range contents with provided HTML snippet
320
+ * @param range - Range to replace
321
+ * @param html - HTML string to insert
322
+ */
323
+ private replaceRangeWithHtml(range: Range, html: string): Range | undefined {
324
+ const fragment = this.createFragmentFromHtml(html);
325
+ const firstInserted = fragment.firstChild ?? null;
326
+ const lastInserted = fragment.lastChild ?? null;
327
+
328
+ range.deleteContents();
329
+
330
+ if (!firstInserted || !lastInserted) {
331
+ return;
332
+ }
333
+
334
+ range.insertNode(fragment);
335
+
336
+ const newRange = document.createRange();
337
+
338
+ newRange.setStartBefore(firstInserted);
339
+ newRange.setEndAfter(lastInserted);
340
+
341
+ return newRange;
342
+ }
343
+
344
+ /**
345
+ * Convert an HTML snippet to a document fragment
346
+ * @param html - HTML string to convert
347
+ */
348
+ private createFragmentFromHtml(html: string): DocumentFragment {
349
+ const template = document.createElement('template');
350
+
351
+ template.innerHTML = html;
352
+
353
+ return template.content;
354
+ }
355
+
356
+ /**
357
+ * Move a temporary marker element outside of an underline ancestor while preserving content order
358
+ * @param marker - Marker element wrapping the selection contents
359
+ * @param underlineElement - Underline ancestor containing the marker
360
+ */
361
+ private moveMarkerOutOfUnderline(marker: HTMLElement, underlineElement: HTMLElement): void {
362
+ const parent = underlineElement.parentNode;
363
+
364
+ if (!parent) {
365
+ return;
366
+ }
367
+
368
+ // Remove empty text nodes to ensure accurate child count
369
+ Array.from(underlineElement.childNodes).forEach((node) => {
370
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').length === 0) {
371
+ node.remove();
372
+ }
373
+ });
374
+
375
+ const isOnlyChild = underlineElement.childNodes.length === 1 && underlineElement.firstChild === marker;
376
+
377
+ if (isOnlyChild) {
378
+ underlineElement.replaceWith(marker);
379
+
380
+ return;
381
+ }
382
+
383
+ const isFirstChild = underlineElement.firstChild === marker;
384
+
385
+ if (isFirstChild) {
386
+ parent.insertBefore(marker, underlineElement);
387
+
388
+ return;
389
+ }
390
+
391
+ const isLastChild = underlineElement.lastChild === marker;
392
+
393
+ if (isLastChild) {
394
+ parent.insertBefore(marker, underlineElement.nextSibling);
395
+
396
+ return;
397
+ }
398
+
399
+ const trailingClone = underlineElement.cloneNode(false) as HTMLElement;
400
+
401
+ while (marker.nextSibling) {
402
+ trailingClone.appendChild(marker.nextSibling);
403
+ }
404
+
405
+ parent.insertBefore(trailingClone, underlineElement.nextSibling);
406
+ parent.insertBefore(marker, trailingClone);
407
+ }
408
+ }
@@ -65,6 +65,13 @@ export class RectangleSelection extends Module {
65
65
  */
66
66
  private mousedown = false;
67
67
 
68
+ /**
69
+ * Set when mousedown starts from a contentEditable element that is within the
70
+ * editor's horizontal content bounds. Prevents the toolbar from re-opening via
71
+ * the BlockHovered event while the user is dragging. Cleared on mouseup.
72
+ */
73
+ private mouseDownWithinBoundsFromContentEditable = false;
74
+
68
75
  /**
69
76
  * Is scrolling now
70
77
  */
@@ -213,6 +220,7 @@ export class RectangleSelection extends Module {
213
220
  */
214
221
  public endSelection(): void {
215
222
  this.mousedown = false;
223
+ this.mouseDownWithinBoundsFromContentEditable = false;
216
224
  this.startX = 0;
217
225
  this.startY = 0;
218
226
  this.anchorBlockIndex = null;
@@ -229,6 +237,18 @@ export class RectangleSelection extends Module {
229
237
  return this.isRectSelectionActivated || this.mousedown;
230
238
  }
231
239
 
240
+ /**
241
+ * Returns true when the user pressed down a mouse button within the editor's horizontal
242
+ * content bounds, even if the click originated on a contentEditable element (in which
243
+ * case rubber-band selection is not started but the toolbar is still closed).
244
+ *
245
+ * Used by the Toolbar module to suppress toolbar reopening while the user is dragging
246
+ * inside the editor content area.
247
+ */
248
+ public get isMouseDownWithinBounds(): boolean {
249
+ return this.mouseDownWithinBoundsFromContentEditable;
250
+ }
251
+
232
252
  /**
233
253
  * Mark that selection is end
234
254
  */
@@ -305,6 +325,24 @@ export class RectangleSelection extends Module {
305
325
 
306
326
  if (!startedFromContentEditable) {
307
327
  this.startSelection(mouseEvent.pageX, mouseEvent.pageY, mouseEvent.shiftKey);
328
+
329
+ return;
330
+ }
331
+
332
+ /**
333
+ * When dragging starts from a contentEditable element, track whether the
334
+ * pointer is within the editor's horizontal content bounds.
335
+ * On the first subsequent mousemove (i.e. when the user actually drags rather
336
+ * than just clicking), the toolbar will be closed and hover-based reopening
337
+ * will be suppressed while the mouse button is held.
338
+ */
339
+ const scrollLeft = this.getScrollLeft();
340
+ const pointerX = mouseEvent.pageX - scrollLeft;
341
+ const contentRect = this.Blok.UI.contentRect;
342
+ const withinEditorHorizontally = pointerX >= contentRect.left && pointerX <= contentRect.right;
343
+
344
+ if (withinEditorHorizontally) {
345
+ this.mouseDownWithinBoundsFromContentEditable = true;
308
346
  }
309
347
  }
310
348
 
@@ -313,6 +351,16 @@ export class RectangleSelection extends Module {
313
351
  * @param {MouseEvent} mouseEvent - mouse event payload
314
352
  */
315
353
  private processMouseMove(mouseEvent: MouseEvent): void {
354
+ /**
355
+ * When the user clicked inside a contentEditable element within the editor's
356
+ * horizontal bounds and is now dragging, close the toolbar on the first move.
357
+ * We defer this to mousemove (rather than mousedown) so that a plain click
358
+ * does not accidentally close the toolbar.
359
+ */
360
+ if (this.mouseDownWithinBoundsFromContentEditable) {
361
+ this.Blok.Toolbar.close();
362
+ }
363
+
316
364
  this.changingRectangle(mouseEvent);
317
365
  this.scrollByZones(mouseEvent.clientY);
318
366
  }
@@ -1009,9 +1009,12 @@ export class Toolbar extends Module<ToolbarNodes> {
1009
1009
  */
1010
1010
  this.eventsDispatcher.on(BlockHovered, (data) => {
1011
1011
  /**
1012
- * Do not move toolbar during drag or rectangle selection operations
1012
+ * Do not move toolbar during drag, rectangle selection, or when the user
1013
+ * started a mouse-drag from within the editor's content area (even if the
1014
+ * drag originated on a contentEditable element, i.e. rubber-band is not
1015
+ * activated but the toolbar was closed and should stay closed).
1013
1016
  */
1014
- if (this.Blok.DragManager.isDragging || this.Blok.RectangleSelection.isRectActivated()) {
1017
+ if (this.Blok.DragManager.isDragging || this.Blok.RectangleSelection.isRectActivated() || this.Blok.RectangleSelection.isMouseDownWithinBounds) {
1015
1018
  return;
1016
1019
  }
1017
1020
 
@@ -165,7 +165,11 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerHandl
165
165
  defaultSwatch.addEventListener('click', () => {
166
166
  onColorSelect(null, mode.key);
167
167
  });
168
- onHover(defaultSwatch, `${i18n.t('tools.marker.default')} ${i18n.t(mode.labelKey).toLowerCase()}`, { placement: 'top' });
168
+ const defaultLabel = i18n.t('tools.colorPicker.defaultSwatchLabel')
169
+ .replace('{default}', i18n.t('tools.marker.default'))
170
+ .replace('{mode}', i18n.t(mode.labelKey).toLowerCase());
171
+
172
+ onHover(defaultSwatch, defaultLabel.charAt(0).toUpperCase() + defaultLabel.slice(1), { placement: 'top' });
169
173
  grid.appendChild(defaultSwatch);
170
174
 
171
175
  for (const preset of presets) {
@@ -193,7 +197,11 @@ export function createColorPicker(options: ColorPickerOptions): ColorPickerHandl
193
197
  swatch.addEventListener('click', () => {
194
198
  onColorSelect(swatchColor, mode.key);
195
199
  });
196
- onHover(swatch, `${i18n.t('tools.colorPicker.color.' + preset.name)} ${i18n.t(mode.labelKey).toLowerCase()}`, { placement: 'top' });
200
+ const colorLabel = i18n.t('tools.colorPicker.colorSwatchLabel')
201
+ .replace('{color}', i18n.t('tools.colorPicker.color.' + preset.name))
202
+ .replace('{mode}', i18n.t(mode.labelKey).toLowerCase());
203
+
204
+ onHover(swatch, colorLabel.charAt(0).toUpperCase() + colorLabel.slice(1), { placement: 'top' });
197
205
  grid.appendChild(swatch);
198
206
  }
199
207
  };
package/src/full.ts CHANGED
@@ -19,6 +19,8 @@ import { BoldInlineTool as Bold } from './components/inline-tools/inline-tool-bo
19
19
  import { ItalicInlineTool as Italic } from './components/inline-tools/inline-tool-italic';
20
20
  import { LinkInlineTool as Link } from './components/inline-tools/inline-tool-link';
21
21
  import { MarkerInlineTool as Marker } from './components/inline-tools/inline-tool-marker';
22
+ import { UnderlineInlineTool as Underline } from './components/inline-tools/inline-tool-underline';
23
+ import { StrikethroughInlineTool as Strikethrough } from './components/inline-tools/inline-tool-strikethrough';
22
24
  import { Header } from './tools/header';
23
25
  import { ListItem as List } from './tools/list';
24
26
  import { Paragraph } from './tools/paragraph';
@@ -35,6 +37,8 @@ export {
35
37
  Italic,
36
38
  Link,
37
39
  Marker,
40
+ Underline,
41
+ Strikethrough,
38
42
  defaultBlockTools,
39
43
  defaultInlineTools,
40
44
  } from './tools';
@@ -68,6 +72,8 @@ export const allTools = {
68
72
  ...defaultTools,
69
73
  bold: { class: Bold },
70
74
  italic: { class: Italic },
71
- link: { class: Link },
72
75
  marker: { class: Marker },
76
+ underline: { class: Underline },
77
+ strikethrough: { class: Strikethrough },
78
+ link: { class: Link },
73
79
  } as const;
@@ -93,8 +93,8 @@ const INLINE_TOOLBAR_TESTID = '[data-blok-testid="inline-toolbar"]';
93
93
  const CONTENTEDITABLE_SELECTOR = '[contenteditable="true"]';
94
94
  const MARKER_TOOL_SELECTOR = '[data-blok-item-name="marker"]';
95
95
  const MARKER_PICKER_SELECTOR = '[data-blok-testid="marker-picker"]';
96
- const MARKER_TAB_BG_SELECTOR = '[data-blok-testid="marker-tab-background-color"]';
97
- const MARKER_GRID_SELECTOR = '[data-blok-testid="marker-grid"]';
96
+ const MARKER_COLOR_SECTION_SELECTOR = '[data-blok-testid="marker-section-color"] button';
97
+ const MARKER_BG_SECTION_SELECTOR = '[data-blok-testid="marker-section-background-color"] button';
98
98
  const TIMEOUT_INIT = { timeout: 5000 };
99
99
  const TIMEOUT_ACTION = { timeout: 5000 };
100
100
 
@@ -411,8 +411,7 @@ export const PickerTextTab: Story = {
411
411
 
412
412
  await waitFor(
413
413
  () => {
414
- const grid = document.querySelector(MARKER_GRID_SELECTOR);
415
- const swatches = grid?.querySelectorAll('button');
414
+ const swatches = document.querySelectorAll(MARKER_COLOR_SECTION_SELECTOR);
416
415
 
417
416
  expect(swatches?.length).toBe(10);
418
417
  },
@@ -434,24 +433,14 @@ export const PickerBackgroundTab: Story = {
434
433
  chromatic: { delay: 500 },
435
434
  },
436
435
  play: async ({ canvasElement, step }) => {
437
- await step('Open marker picker', async () => {
436
+ await step('Open marker picker and verify background section', async () => {
438
437
  await openMarkerPicker(canvasElement);
439
- });
440
-
441
- await step('Switch to background tab', async () => {
442
- const bgTab = document.querySelector(MARKER_TAB_BG_SELECTOR);
443
-
444
- expect(bgTab).toBeInTheDocument();
445
-
446
- if (bgTab) {
447
- simulateClick(bgTab);
448
- }
449
438
 
450
439
  await waitFor(
451
440
  () => {
452
- const grid = document.querySelector(MARKER_GRID_SELECTOR);
441
+ const swatches = document.querySelectorAll(MARKER_BG_SECTION_SELECTOR);
453
442
 
454
- expect(grid?.querySelectorAll('button')?.length).toBe(10);
443
+ expect(swatches?.length).toBe(10);
455
444
  },
456
445
  TIMEOUT_ACTION
457
446
  );
@@ -475,7 +464,7 @@ export const PickerDefaultButton: Story = {
475
464
 
476
465
  await waitFor(
477
466
  () => {
478
- const defaultBtn = document.querySelector('[data-blok-testid="marker-default-btn"]');
467
+ const defaultBtn = document.querySelector('[data-blok-testid="marker-swatch-color-default"]');
479
468
 
480
469
  expect(defaultBtn).toBeInTheDocument();
481
470
  },
@@ -502,13 +491,12 @@ export const PickerNoActiveSwatch: Story = {
502
491
 
503
492
  await waitFor(
504
493
  () => {
505
- const grid = document.querySelector(MARKER_GRID_SELECTOR);
506
- const swatches = grid?.querySelectorAll('button');
494
+ const swatches = document.querySelectorAll(MARKER_COLOR_SECTION_SELECTOR);
507
495
 
508
496
  expect(swatches?.length).toBe(10);
509
497
 
510
498
  swatches?.forEach((swatch) => {
511
- expect(swatch.className).not.toContain('ring-black/30');
499
+ expect(swatch.className).not.toContain('ring-swatch-ring-hover');
512
500
  });
513
501
  },
514
502
  TIMEOUT_ACTION
@@ -581,9 +569,9 @@ export const PickerActiveTextSwatch: Story = {
581
569
  // Verify the red swatch has the active ring
582
570
  await waitFor(
583
571
  () => {
584
- const redSwatch = document.querySelector('[data-blok-testid="marker-swatch-red"]');
572
+ const redSwatch = document.querySelector('[data-blok-testid="marker-swatch-color-red"]');
585
573
 
586
- expect(redSwatch?.className).toContain('ring-black/30');
574
+ expect(redSwatch?.className).toContain('ring-swatch-ring-hover');
587
575
  },
588
576
  TIMEOUT_ACTION
589
577
  );
@@ -653,21 +641,12 @@ export const PickerActiveBackgroundSwatch: Story = {
653
641
  TIMEOUT_ACTION
654
642
  );
655
643
 
656
- // Switch to background tab
657
- const bgTab = document.querySelector(MARKER_TAB_BG_SELECTOR);
658
-
659
- expect(bgTab).toBeInTheDocument();
660
-
661
- if (bgTab) {
662
- simulateClick(bgTab);
663
- }
664
-
665
644
  // Verify the orange swatch has the active ring
666
645
  await waitFor(
667
646
  () => {
668
- const orangeSwatch = document.querySelector('[data-blok-testid="marker-swatch-orange"]');
647
+ const orangeSwatch = document.querySelector('[data-blok-testid="marker-swatch-background-color-orange"]');
669
648
 
670
- expect(orangeSwatch?.className).toContain('ring-black/30');
649
+ expect(orangeSwatch?.className).toContain('ring-swatch-ring-hover');
671
650
  },
672
651
  TIMEOUT_ACTION
673
652
  );
@@ -1041,7 +1041,7 @@
1041
1041
  */
1042
1042
  @utility blok-block {
1043
1043
  /* Base Blok styles - applied to block tool wrappers */
1044
- @apply py-[3px] px-[2px] [&::-webkit-input-placeholder]:leading-normal!;
1044
+ @apply py-[7px] px-[2px] [&::-webkit-input-placeholder]:leading-normal!;
1045
1045
  }
1046
1046
  @utility blok-inline-tool-button {
1047
1047
  /* Inline Tools styles */