@layers-app/editor 0.7.42 → 0.7.44
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 +830 -830
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +4 -5
- package/dist/index.js +2 -2
- package/dist/{layers.zCeo5Zqb.js → layers.03rOzGz6.js} +1 -1
- package/dist/{layers.Adi0rjqQ.js → layers.2ki87-LU.js} +1 -1
- package/dist/{layers.3C1Klxc_.js → layers.2o06iRgp.js} +3 -3
- package/dist/{layers.DBCm9UUG.js → layers.5oOWOsbG.js} +1 -1
- package/dist/{layers.OwGkAQYb.js → layers.8AV9TJ5y.js} +1 -1
- package/dist/{layers.BEfZHHsH.js → layers.8KaGKAU3.js} +4 -4
- package/dist/{layers.CjsGkp9b.js → layers.9URcRock.js} +1 -1
- package/dist/{layers.DEFnU0xk.js → layers.B-dneuft.js} +3 -3
- package/dist/{layers.D21OVdxe.js → layers.B1fchoGW.js} +1 -1
- package/dist/{layers.Bz8pEB45.js → layers.B2cspFhx.js} +1 -1
- package/dist/{layers.CYaD93jt.js → layers.B6soS223.js} +1 -1
- package/dist/{layers.BVy4SrEU.js → layers.B780IvgC.js} +1 -1
- package/dist/{layers.D1J-APYC.js → layers.B9UunCzG.js} +1 -1
- package/dist/{layers.BEW8izM3.js → layers.B9rQcczY.js} +2 -2
- package/dist/{layers.BglcFqY8.js → layers.BBcDfITy.js} +1 -1
- package/dist/{layers.c4bvOvfT.js → layers.BBhmfZot.js} +1 -1
- package/dist/{layers.wobc6Wpr.js → layers.BDqHBDOj.js} +1 -1
- package/dist/{layers.OsDF6ak0.js → layers.BFRbDTjY.js} +1 -1
- package/dist/{layers.DW-O-tqO.js → layers.BIUiVID4.js} +1 -1
- package/dist/{layers.uFVAIups.js → layers.BKamPs3H.js} +1 -1
- package/dist/{layers.BLOnMtzT.js → layers.BMWMvKH-.js} +2 -2
- package/dist/{layers.CVV0nJuL.js → layers.BMxAun-L.js} +1 -1
- package/dist/{layers.BH-tVGLm.js → layers.BOMgDmQW.js} +1 -1
- package/dist/{layers.BkLHGcim.js → layers.BPwGnx6H.js} +1 -1
- package/dist/{layers.C2EIWGlW.js → layers.BQX_W7-N.js} +1 -1
- package/dist/{layers.BXN70Z1p.js → layers.BRBbDnyO.js} +1 -1
- package/dist/{layers.BBFsl73n.js → layers.BRU1WOyl.js} +2 -2
- package/dist/{layers.CycK0cvs.js → layers.BUP7J0N1.js} +7 -7
- package/dist/{layers.DYfD5_3p.js → layers.BWYHKKSc.js} +1 -1
- package/dist/{layers.B9Trszhw.js → layers.B_5Gf5vh.js} +1 -1
- package/dist/{layers.DoNkrTqt.js → layers.Be7sKoi1.js} +1 -1
- package/dist/{layers.CvFbPFqH.js → layers.BewWHVmc.js} +1 -1
- package/dist/{layers.CMBPgp1f.js → layers.Bhbf4Voy.js} +5 -5
- package/dist/{layers.BZNfbtoG.js → layers.BhgyYcry.js} +1 -1
- package/dist/{layers.DCfXS6Lt.js → layers.BjqXvHbh.js} +1 -1
- package/dist/{layers.tRJ6agBB.js → layers.Bl153Elb.js} +3 -3
- package/dist/{layers.CedPOo7p.js → layers.BlVr0EB6.js} +1 -1
- package/dist/{layers.D2pCrIEI.js → layers.BmBx-JQP.js} +1 -1
- package/dist/{layers.LIfCWmjz.js → layers.BnDdlKo4.js} +1 -1
- package/dist/{layers.Whgggs9u.js → layers.BnWlIV7l.js} +1 -1
- package/dist/{layers.CdFlGWzu.js → layers.BpxcN2Id.js} +1 -1
- package/dist/{layers.Bvw4UsSb.js → layers.Bt5BEQ6h.js} +1 -1
- package/dist/{layers.C5jguaPk.js → layers.Bva7u9LH.js} +1 -1
- package/dist/{layers.BVYs5P5-.js → layers.Bw9PyPP3.js} +1 -1
- package/dist/{layers.t1vS44PF.js → layers.BxxWjVSm.js} +1 -1
- package/dist/{layers.DI2kTjPo.js → layers.ByfnxHEU.js} +1 -1
- package/dist/{layers.C0nmXpTl.js → layers.BzrUktHE.js} +1 -1
- package/dist/{layers.Dl2llq6u.js → layers.Bzx6XYuC.js} +1 -1
- package/dist/{layers.D6zQl_eN.js → layers.C-oUoVP8.js} +1 -1
- package/dist/{layers.v4srTVt8.js → layers.C-v8184a.js} +1 -1
- package/dist/{layers.-q1GBDww.js → layers.C2DLU3k6.js} +4 -4
- package/dist/{layers.C9Zpsdqs.js → layers.C4n-fEss.js} +1 -1
- package/dist/{layers.CecrMT1N.js → layers.C8LHt7Vy.js} +1 -1
- package/dist/{layers.OIJOUM7Y.js → layers.C8i9MTMT.js} +1 -1
- package/dist/{layers.grgyE8UI.js → layers.C8sUTigy.js} +1 -1
- package/dist/{layers.Bmx4iER7.js → layers.C9xAxi33.js} +1 -1
- package/dist/{layers.-WYTYnWK.js → layers.CBBwxyeE.js} +1 -1
- package/dist/{layers.Djizg_2G.js → layers.CD1cqEz5.js} +1 -1
- package/dist/{layers.DkBUd60x.js → layers.CDMZWhfy.js} +1 -1
- package/dist/{layers.CLul97IO.js → layers.CEGoZtyz.js} +1 -1
- package/dist/{layers.tiMQzy3i.js → layers.CF5fav1y.js} +1 -1
- package/dist/{layers.B3Fg5aAW.js → layers.CL2zU9v_.js} +1 -1
- package/dist/{layers.BQ7gI5TW.js → layers.CMF1lFeu.js} +4 -4
- package/dist/{layers.nMhw2kqG.js → layers.CN4h4dWv.js} +1 -1
- package/dist/{layers.DAdJVsDC.js → layers.COskFe6S.js} +1 -1
- package/dist/{layers.BMCUjVZr.js → layers.CPYfnqn5.js} +1 -1
- package/dist/{layers.DYnNIoY-.js → layers.CQGa02SB.js} +1 -1
- package/dist/{layers.C0-gBZTZ.js → layers.CS1a09mB.js} +1 -1
- package/dist/{layers.iux8b4-i.js → layers.CSABgVEk.js} +1 -1
- package/dist/{layers.CTQCWf4-.js → layers.CSPqTpuh.js} +1 -1
- package/dist/{layers.CekrwdFP.js → layers.CUJquY9u.js} +1 -1
- package/dist/{layers.CZuSWcxs.js → layers.CVpmcvSk.js} +1 -1
- package/dist/{layers.1ceHDx8C.js → layers.CXNwKrxy.js} +1 -1
- package/dist/{layers.BH8KgG6N.js → layers.CXbg7PbM.js} +1 -1
- package/dist/{layers.p3FInBUH.js → layers.CXtLmfFd.js} +1 -1
- package/dist/{layers.B-1QcpX-.js → layers.CYQ-eDkR.js} +1 -1
- package/dist/{layers.acMZatdI.js → layers.CYlMzZJI.js} +4 -4
- package/dist/{layers.Cjt5Oyv0.js → layers.C_6t44t9.js} +1 -1
- package/dist/layers.C_AqJZgj.js +1 -0
- package/dist/{layers.Biz9Si-B.js → layers.C_JAqhrY.js} +2 -2
- package/dist/{layers.4wtUlhUd.js → layers.C_l6mWJZ.js} +1 -1
- package/dist/{layers.CZsPG2AG.js → layers.Cbn9kFtP.js} +4 -4
- package/dist/{layers.CoCd0wHE.js → layers.CdC2YLb3.js} +4 -4
- package/dist/{layers.v6qktDbk.js → layers.Ce68IynP.js} +1 -1
- package/dist/{layers.pkM5fPPJ.js → layers.Ced46X3q.js} +1 -1
- package/dist/{layers.DSr5yUZW.js → layers.CgAndu8u.js} +3 -3
- package/dist/{layers.CZhug0Ib.js → layers.CgKZ3PQQ.js} +1 -1
- package/dist/{layers.F0Htc0vN.js → layers.CjF61D9D.js} +1 -1
- package/dist/{layers.C51WO3G3.js → layers.Ck_mYNSX.js} +1 -1
- package/dist/{layers.BMTJR5V-.js → layers.Ckb_FqEw.js} +1 -1
- package/dist/{layers.Dm5ngCMh.js → layers.ClK-YSEj.js} +1 -1
- package/dist/{layers.BkoLFzJh.js → layers.ClPMD0vL.js} +1 -1
- package/dist/{layers.NnlPWYqr.js → layers.Cq3WhnzE.js} +1 -1
- package/dist/{layers.CE3HvIZz.js → layers.CrM-3BQr.js} +1 -1
- package/dist/{layers.al-lVgbb.js → layers.CsJQzt0K.js} +4 -4
- package/dist/{layers.BNMx0RP8.js → layers.CsTbIH2o.js} +8021 -7888
- package/dist/{layers.DdH20ja-.js → layers.Cux05MKl.js} +1 -1
- package/dist/{layers.D_JsgZkZ.js → layers.CwZFs1PA.js} +1 -1
- package/dist/{layers.C3590-Yq.js → layers.Cxlebi7D.js} +1 -1
- package/dist/{layers.3seCFp5m.js → layers.Cy9EF2DN.js} +1 -1
- package/dist/{layers.CuXemfyF.js → layers.D1hB76Bq.js} +1 -1
- package/dist/{layers.D4BNh-Vu.js → layers.D3RHWYgt.js} +1 -1
- package/dist/{layers.CZTIxh4r.js → layers.D3qdetie.js} +1 -1
- package/dist/{layers.CkB3JIeA.js → layers.D3xtqcGm.js} +1 -1
- package/dist/{layers.DVGedfcw.js → layers.D54e-Khu.js} +1 -1
- package/dist/{layers.DQ3VncXs.js → layers.D5EnJuSD.js} +1 -1
- package/dist/{layers.GXgxXDXY.js → layers.D661e9Xc.js} +4 -4
- package/dist/{layers.vXybTs00.js → layers.D7cIx_9y.js} +1 -1
- package/dist/{layers.BgptGTds.js → layers.D7v-bI_M.js} +3 -3
- package/dist/layers.DA37YYAX.js +1 -0
- package/dist/{layers.BQFb7rfK.js → layers.DBFtWsLd.js} +1 -1
- package/dist/{layers.BDc6D4Kr.js → layers.DBspJRLt.js} +1 -1
- package/dist/{layers.DoLRPac0.js → layers.DCYPhLVi.js} +1 -1
- package/dist/{layers.NlLb5YHh.js → layers.DCeGyOw0.js} +1 -1
- package/dist/{layers.DjN9QZn7.js → layers.DDBS2bZp.js} +1 -1
- package/dist/{layers.5iOhFhj7.js → layers.DEV2RzXF.js} +1 -1
- package/dist/{layers.h47AEIVe.js → layers.DFOzrTFb.js} +1 -1
- package/dist/{layers.yGzN1dU_.js → layers.DGbx9JE5.js} +1 -1
- package/dist/{layers.khlOiajc.js → layers.DH1xK3c2.js} +6 -6
- package/dist/{layers.Drav_qcm.js → layers.DH2Fx227.js} +1 -1
- package/dist/{layers.IGr8EpK-.js → layers.DH7NXYKd.js} +4 -4
- package/dist/{layers.Bqb7FFSv.js → layers.DHOsMTqa.js} +1 -1
- package/dist/{layers.CjfBH299.js → layers.DJIT4Yuh.js} +1 -1
- package/dist/{layers.C7oQBRE0.js → layers.DJteL1WD.js} +1 -1
- package/dist/layers.DN58BT2N.js +129 -0
- package/dist/{layers.Bk1DPbZ8.js → layers.DNRniJ59.js} +1 -1
- package/dist/{layers.BtXbD2Gq.js → layers.DOZhGFQj.js} +1 -1
- package/dist/{layers.D4ythG4z.js → layers.DP6Pb86W.js} +1 -1
- package/dist/{layers.DEj22Eqd.js → layers.DQpfAS9_.js} +1 -1
- package/dist/{layers.BUNgAzt2.js → layers.DSBfJxui.js} +3 -3
- package/dist/{layers.BY6OuHj4.js → layers.DUulRicy.js} +2 -2
- package/dist/{layers.mRI_aHXT.js → layers.DXLQ2q0o.js} +5 -5
- package/dist/{layers.CcFitHem.js → layers.DXqg60IE.js} +1 -1
- package/dist/{layers.CcmC5PPf.js → layers.DZkyqPUI.js} +1 -1
- package/dist/{layers.C3Jl3qNt.js → layers.D_MAZnF5.js} +1 -1
- package/dist/{layers.CZNTIR1l.js → layers.DbMx76Zo.js} +1 -1
- package/dist/{layers.CqacMvDb.js → layers.DcYYTMWk.js} +1 -1
- package/dist/{layers.B5MsCUAo.js → layers.DenHSAM-.js} +1 -1
- package/dist/{layers.i6vT0g7A.js → layers.Dfg_GwXo.js} +1 -1
- package/dist/{layers.DcVZfpJ_.js → layers.Dfv6IVuG.js} +1 -1
- package/dist/{layers.CJv_mbZM.js → layers.Di5XQ7RG.js} +1 -1
- package/dist/{layers.BFqCPvxp.js → layers.DiLG4f1T.js} +1 -1
- package/dist/{layers.Cy-qxBnZ.js → layers.Dksb8xWL.js} +8 -8
- package/dist/{layers.Dhk83Ipt.js → layers.DnTHkHku.js} +1 -1
- package/dist/{layers.BLEOXZZa.js → layers.DnbGJb7n.js} +1 -1
- package/dist/{layers.D6yot_8p.js → layers.Dp8hIITu.js} +1 -1
- package/dist/{layers.D4b_7FiT.js → layers.DrGILrJe.js} +1 -1
- package/dist/layers.DrPZhd4b.js +8 -0
- package/dist/{layers.C7ZExN5Y.js → layers.DsCDPcQF.js} +1 -1
- package/dist/{layers.BWalyAnV.js → layers.DsLXYKq7.js} +1 -1
- package/dist/{layers.ACvQFP_B.js → layers.DssLE6Ze.js} +1 -1
- package/dist/{layers.CsniuqJV.js → layers.DtK0Svqg.js} +1 -1
- package/dist/{layers.DYyHK0aB.js → layers.DtNKNbtb.js} +2 -2
- package/dist/{layers.BPVUggs1.js → layers.DuuzgAVb.js} +1 -1
- package/dist/{layers.Eux2URlS.js → layers.DuyNXGEO.js} +1 -1
- package/dist/{layers.DVt-kINL.js → layers.DvSwjIBG.js} +1 -1
- package/dist/{layers.DWX7q_oj.js → layers.DwPc2_v9.js} +3 -3
- package/dist/{layers.CK60g40c.js → layers.DyK8xvHc.js} +1 -1
- package/dist/{layers.CFBWi5RS.js → layers.G5fXkV2g.js} +1 -1
- package/dist/{layers.CvDpMrop.js → layers.GRwgnZ0K.js} +1 -1
- package/dist/{layers.BKlP3Rp5.js → layers.H9GbooLl.js} +1 -1
- package/dist/{layers.BjcNTuJq.js → layers.Ii0kosET.js} +1 -1
- package/dist/{layers.DA5MuCPH.js → layers.IsBK41F6.js} +1 -1
- package/dist/{layers.CSzfUATt.js → layers.JFRyjmr3.js} +3 -3
- package/dist/{layers.DSu6Ogaz.js → layers.L1EEfd1U.js} +2 -2
- package/dist/{layers.Bp8T5USV.js → layers.LgOKQKa4.js} +1 -1
- package/dist/{layers.KKW4-XQ_.js → layers.NsxwBYck.js} +3 -3
- package/dist/{layers.Dz_CI_Fa.js → layers.OqunHjml.js} +1 -1
- package/dist/{layers.C1QVX6Xl.js → layers.OziSx6JY.js} +24 -24
- package/dist/{layers.CAJC-nl3.js → layers.PW9J5Jxo.js} +1 -1
- package/dist/{layers.GAs5-_U5.js → layers.S2akqscS.js} +1 -1
- package/dist/{layers.BqMVmdXE.js → layers.SGT8YAPE.js} +1 -1
- package/dist/{layers.DZfbpF3V.js → layers.TD8A7YjM.js} +1 -1
- package/dist/{layers.BVRjFlUC.js → layers.UtIqQK19.js} +107 -89
- package/dist/{layers.CFywxPrt.js → layers.VIZ2Xa30.js} +1 -1
- package/dist/{layers.ragNiaN3.js → layers.WAvmJfLV.js} +1 -1
- package/dist/{layers.DILwdL6t.js → layers.XQpyNRW5.js} +1 -1
- package/dist/{layers.o_LON7gL.js → layers.Xco5GVBh.js} +1 -1
- package/dist/{layers.DX7H0708.js → layers.Y0BjACCI.js} +1 -1
- package/dist/{layers.D6WGW5mD.js → layers.YTMr1YyH.js} +1 -1
- package/dist/{layers.CPEFd37W.js → layers._bAvXI7Q.js} +3 -3
- package/dist/{layers.DSKJhjR1.js → layers._mqWoKOG.js} +1 -1
- package/dist/{layers.mkqQBzFk.js → layers.al8Vg6zt.js} +1 -1
- package/dist/layers.dyRJvH-s.js +6 -0
- package/dist/{layers.DKyAq4Ov.js → layers.eB1U5Jl6.js} +1 -1
- package/dist/{layers.DBA5cNWB.js → layers.euNtZlgm.js} +1 -1
- package/dist/{layers.CNDPMdPL.js → layers.fChmSQjC.js} +1 -1
- package/dist/layers.feSI_7Kx.js +1 -0
- package/dist/{layers.BHaTB_OU.js → layers.frthGoDK.js} +1 -1
- package/dist/{layers.BQfDZ1XY.js → layers.gqLoI9xW.js} +1 -1
- package/dist/{layers.HXifgbRd.js → layers.hpuqkyYN.js} +1 -1
- package/dist/{layers.DZo7Eyhw.js → layers.iN3mYsr5.js} +1 -1
- package/dist/{layers.Df1TUgIu.js → layers.jjNsAaLS.js} +1 -1
- package/dist/{layers.Cpmet8vT.js → layers.l6EuAdoo.js} +1 -1
- package/dist/{layers.AfGBpUn1.js → layers.lXs3RYd3.js} +1 -1
- package/dist/{layers.DsnNnwhL.js → layers.liB8aZ3C.js} +1 -1
- package/dist/{layers.DuqNbO18.js → layers.qffllMmW.js} +1 -1
- package/dist/{layers.JOfgipnP.js → layers.r1jKcKBb.js} +1 -1
- package/dist/{layers.B5GBTm7w.js → layers.rFy6_B28.js} +1 -1
- package/dist/{layers.0GwlUhgo.js → layers.rq1vUYAs.js} +1 -1
- package/dist/{layers.C2_od0Dc.js → layers.sIeIPQd4.js} +1 -1
- package/dist/{layers.D_4R1ZSo.js → layers.sdxt2Nuu.js} +1 -1
- package/dist/{layers.9jKVPcdd.js → layers.t8MaTGN8.js} +1 -1
- package/dist/{layers.Ct9Zf_S7.js → layers.wKJ5yZVV.js} +3 -3
- package/dist/{layers.DSIPnKcj.js → layers.x9brO2TV.js} +1 -1
- package/dist/{layers.C-YJUyvL.js → layers.xQ1jNHSB.js} +3 -3
- package/package.json +160 -160
- package/dist/layers.BVE5w1XI.js +0 -8
- package/dist/layers.BcCP-58E.js +0 -1
- package/dist/layers.C04ba_Dk.js +0 -129
- package/dist/layers.D_Q8YOfJ.js +0 -1
- package/dist/layers.DqqFKOJp.js +0 -6
- package/dist/layers.DrDFWHc3.js +0 -1
package/README.md
CHANGED
|
@@ -1,830 +1,830 @@
|
|
|
1
|
-
# LayersTextEditor
|
|
2
|
-
|
|
3
|
-
LayersTextEditor is a text editor for web applications written in JavaScript, with a focus on reliability, accessibility, and performance.
|
|
4
|
-
|
|
5
|
-
<details>
|
|
6
|
-
<summary>
|
|
7
|
-
🚀 Quick Start
|
|
8
|
-
</summary>
|
|
9
|
-
|
|
10
|
-
## Installation
|
|
11
|
-
|
|
12
|
-
To install the package, run one of the following commands:
|
|
13
|
-
|
|
14
|
-
### Use npm:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @layers-app/editor
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
> If you plan to use the Swagger node, install the additional package: `npm install swagger-ui-react`.
|
|
21
|
-
|
|
22
|
-
### Use yarn:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
yarn add @layers-app/editor
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Initializing the text editor
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
import { Editor } from '@layers-app/editor';
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
By default, LayersTextEditor works with an object and can return either an object or HTML.
|
|
35
|
-
|
|
36
|
-
Example with an object:
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
const text = 'Hello world';
|
|
40
|
-
|
|
41
|
-
const json = {
|
|
42
|
-
root: {
|
|
43
|
-
children: [
|
|
44
|
-
{
|
|
45
|
-
children: [
|
|
46
|
-
{
|
|
47
|
-
detail: 0,
|
|
48
|
-
format: 0,
|
|
49
|
-
mode: 'normal',
|
|
50
|
-
style: '',
|
|
51
|
-
text: text,
|
|
52
|
-
type: 'text',
|
|
53
|
-
version: 1,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
direction: 'ltr',
|
|
57
|
-
format: '',
|
|
58
|
-
indent: 0,
|
|
59
|
-
type: 'paragraph',
|
|
60
|
-
version: 1,
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
direction: 'ltr',
|
|
64
|
-
format: '',
|
|
65
|
-
indent: 0,
|
|
66
|
-
type: 'root',
|
|
67
|
-
version: 1,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const onChange = (
|
|
72
|
-
data, // json
|
|
73
|
-
) => <Editor initialContent={json} onChange={onChange} />;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
You can also pass an HTML string to the editor.
|
|
77
|
-
|
|
78
|
-
Example with HTML:
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
const html = `
|
|
82
|
-
<h2 dir="ltr" style="text-align: left;">
|
|
83
|
-
<span style="background-color: rgb(248, 231, 28); font-family: "Trebuchet MS"; white-space: pre-wrap;">Hello</span>
|
|
84
|
-
</h2>
|
|
85
|
-
<h2 dir="ltr">
|
|
86
|
-
<br>
|
|
87
|
-
</h2>
|
|
88
|
-
<p dir="ltr">
|
|
89
|
-
<br>
|
|
90
|
-
</p>
|
|
91
|
-
<p dir="ltr">
|
|
92
|
-
<span style="font-size: 21px; white-space: pre-wrap;">world</span>
|
|
93
|
-
</p>
|
|
94
|
-
`
|
|
95
|
-
|
|
96
|
-
const onChange = (data) => // json
|
|
97
|
-
|
|
98
|
-
<Editor initialContent={html} onChange={onChange} />
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
The output of the data in the `onChange` function is controlled by the **outputFormat** property. **outputFormat** can be either "html" or "json". Example with **outputFormat**:
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
const html = `
|
|
105
|
-
<h2 dir="ltr" style="text-align: left;">
|
|
106
|
-
<span style="background-color: rgb(248, 231, 28); font-family: "Trebuchet MS"; white-space: pre-wrap;">Hello</span>
|
|
107
|
-
</h2>
|
|
108
|
-
<h2 dir="ltr">
|
|
109
|
-
<br>
|
|
110
|
-
</h2>
|
|
111
|
-
<p dir="ltr">
|
|
112
|
-
<br>
|
|
113
|
-
</p>
|
|
114
|
-
<p dir="ltr">
|
|
115
|
-
<span style="font-size: 21px; white-space: pre-wrap;">world</span>
|
|
116
|
-
</p>
|
|
117
|
-
`
|
|
118
|
-
|
|
119
|
-
const onChange = (data: string, text?: string) => {
|
|
120
|
-
// data - html from editor
|
|
121
|
-
// text - text from editor
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<Editor initialContent={html} outputFormat="html" onChange={onChange} />
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
</details>
|
|
129
|
-
|
|
130
|
-
<details>
|
|
131
|
-
<summary>
|
|
132
|
-
🎨 StylesProvider
|
|
133
|
-
</summary>
|
|
134
|
-
|
|
135
|
-
Use **StylesProvider** to add styling to your HTML content.
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
<StylesProvider>
|
|
139
|
-
<div
|
|
140
|
-
dangerouslySetInnerHTML={{ __html: '<p>Your html here</p>' }}
|
|
141
|
-
/>
|
|
142
|
-
</StylesProvider>
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
</details>
|
|
146
|
-
|
|
147
|
-
<details>
|
|
148
|
-
<summary>
|
|
149
|
-
🖼️ File upload
|
|
150
|
-
</summary>
|
|
151
|
-
|
|
152
|
-
## Image upload
|
|
153
|
-
|
|
154
|
-
To start working with image uploads, use the **fetchUploadMedia** function, which takes three parameters: **file**, **success**, and **error**. After successfully uploading the image to your service, you should call the **success** function and pass two required arguments: the **URL** of the image and its **ID**.
|
|
155
|
-
Optional: You can also pass two optional parameters: **signal** and **onProgress**. The **signal** allows you to cancel an ongoing upload using an AbortController, and **onProgress** provides the current upload progress in percent — useful for displaying a progress bar or loading state.
|
|
156
|
-
```
|
|
157
|
-
const fetchUploadMedia = async (
|
|
158
|
-
file: File,
|
|
159
|
-
success: (url: string, id: string) => void,
|
|
160
|
-
error?: (error?: Error) => void
|
|
161
|
-
) => {
|
|
162
|
-
const formData = new FormData();
|
|
163
|
-
formData.append('File', file);
|
|
164
|
-
formData.append('FileAccessModifier', '0');
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const response = await fetch('/api/v1/Files/Upload', {
|
|
168
|
-
method: 'POST',
|
|
169
|
-
body: formData,
|
|
170
|
-
credentials: 'include'
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
throw new Error('File upload failed');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const data = await response.json();
|
|
178
|
-
const { Id, Url } = data;
|
|
179
|
-
|
|
180
|
-
success(Url, Id);
|
|
181
|
-
} catch (err) {
|
|
182
|
-
if (error) {
|
|
183
|
-
if (err instanceof Error) {
|
|
184
|
-
error(err);
|
|
185
|
-
} else {
|
|
186
|
-
error(new Error('An unknown error occurred'));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const fetchUploadMedia = async (
|
|
193
|
-
file: File,
|
|
194
|
-
success: (url: string, id: string, data: any) => void,
|
|
195
|
-
error?: (err: Error) => void,
|
|
196
|
-
signal?: AbortSignal,
|
|
197
|
-
onProgress?: (percent: number) => void,
|
|
198
|
-
) => {
|
|
199
|
-
const formData = new FormData();
|
|
200
|
-
formData.append('File', file);
|
|
201
|
-
formData.append('FileAccessModifier', '0');
|
|
202
|
-
|
|
203
|
-
const xhr = new XMLHttpRequest();
|
|
204
|
-
xhr.open('POST', '/api/v1/Files/Upload');
|
|
205
|
-
xhr.withCredentials = true;
|
|
206
|
-
|
|
207
|
-
if (signal) signal.addEventListener('abort', () => xhr.abort());
|
|
208
|
-
if (onProgress) {
|
|
209
|
-
xhr.upload.onprogress = (e) => {
|
|
210
|
-
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
xhr.onload = () => {
|
|
215
|
-
try {
|
|
216
|
-
const data = JSON.parse(xhr.responseText);
|
|
217
|
-
success(`/v1/attachments/${data.id}`, data.id, data);
|
|
218
|
-
} catch {
|
|
219
|
-
error?.(new Error('Invalid response'));
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
xhr.onerror = () => error?.(new Error('Upload error')));
|
|
224
|
-
xhr.send(formData);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<Editor
|
|
229
|
-
...props
|
|
230
|
-
fetchUploadMedia={fetchUploadMedia}
|
|
231
|
-
/>
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Image Deletion
|
|
235
|
-
|
|
236
|
-
To have greater control over image deletion, pass an optional function **fetchDeleteMedia** to the editor, which accepts three parameters: **id**, **success**, and **error**. After successfully deleting the image from your service, the **success** function should be called.
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
const fetchDeleteMedia = async (
|
|
240
|
-
id: string,
|
|
241
|
-
success: () => void,
|
|
242
|
-
error?: (error?: Error) => void
|
|
243
|
-
) => {
|
|
244
|
-
const body = { Ids: [id] };
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const response = await fetch('/api/v1/Documents/Delete', {
|
|
248
|
-
method: 'POST',
|
|
249
|
-
headers: {
|
|
250
|
-
'Content-Type': 'application/json'
|
|
251
|
-
},
|
|
252
|
-
body: JSON.stringify(body),
|
|
253
|
-
credentials: 'include'
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
await response.json();
|
|
257
|
-
success();
|
|
258
|
-
} catch (err) {
|
|
259
|
-
if (error) {
|
|
260
|
-
if (err instanceof Error) {
|
|
261
|
-
error(err);
|
|
262
|
-
} else {
|
|
263
|
-
error(new Error('An unknown error occurred'));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
<Editor
|
|
270
|
-
...props
|
|
271
|
-
fetchUploadMedia={fetchUploadMedia}
|
|
272
|
-
fetchDeleteMedia={fetchUploadMedia}
|
|
273
|
-
/>
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Additional options for working with image uploads.
|
|
277
|
-
|
|
278
|
-
```
|
|
279
|
-
import { Editor, Dropzone } from "@sinups/editor-dsd";
|
|
280
|
-
|
|
281
|
-
const Content = () => (
|
|
282
|
-
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
|
283
|
-
{/*
|
|
284
|
-
The components Dropzone.Accept, Dropzone.Reject, and Dropzone.Idle are visible only when the user performs specific actions:
|
|
285
|
-
|
|
286
|
-
Dropzone.Accept is visible only when the user drags a file that can be accepted into the drop zone.
|
|
287
|
-
Dropzone.Reject is visible only when the user drags a file that cannot be accepted into the drop zone.
|
|
288
|
-
Dropzone.Idle is visible when the user is not dragging any file into the drop zone.
|
|
289
|
-
*/}
|
|
290
|
-
<Dropzone.Accept>
|
|
291
|
-
<IconUpload
|
|
292
|
-
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)' }}
|
|
293
|
-
stroke={1.5}
|
|
294
|
-
/>
|
|
295
|
-
</Dropzone.Accept>
|
|
296
|
-
<Dropzone.Reject>
|
|
297
|
-
<IconX
|
|
298
|
-
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)' }}
|
|
299
|
-
stroke={1.5}
|
|
300
|
-
/>
|
|
301
|
-
</Dropzone.Reject>
|
|
302
|
-
<Dropzone.Idle>
|
|
303
|
-
<IconPhoto
|
|
304
|
-
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)' }}
|
|
305
|
-
stroke={1.5}
|
|
306
|
-
/>
|
|
307
|
-
</Dropzone.Idle>
|
|
308
|
-
|
|
309
|
-
<div>
|
|
310
|
-
<Text size="xl" inline>
|
|
311
|
-
Drag images here or click to select files
|
|
312
|
-
</Text>
|
|
313
|
-
<Text size="sm" c="dimmed" inline mt={7}>
|
|
314
|
-
Attach as many files as you want, each file must not exceed{' '} {maxFileSize} МБ.
|
|
315
|
-
</Text>
|
|
316
|
-
</div>
|
|
317
|
-
</Group>
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
<Editor
|
|
321
|
-
...props
|
|
322
|
-
fetchUploadMedia={fetchUploadMedia}
|
|
323
|
-
contentModalUploadImage={Content}
|
|
324
|
-
maxFileSize={5}
|
|
325
|
-
maxImageSizeError={() => {}}
|
|
326
|
-
/>
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
## File upload
|
|
330
|
-
|
|
331
|
-
For uploading a file or audio, you might need the third parameter "data".
|
|
332
|
-
|
|
333
|
-
```
|
|
334
|
-
const fetchUploadMedia = async (
|
|
335
|
-
file: File,
|
|
336
|
-
success: (url: string, id: string, data?: {
|
|
337
|
-
contentType: string;
|
|
338
|
-
fileSize: string;
|
|
339
|
-
originalFileName: string;
|
|
340
|
-
}) => void,
|
|
341
|
-
error?: (error?: Error) => void
|
|
342
|
-
) => {
|
|
343
|
-
const formData = new FormData();
|
|
344
|
-
formData.append('File', file);
|
|
345
|
-
formData.append('FileAccessModifier', '0');
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
const response = await fetch('/api/v1/Files/Upload', {
|
|
349
|
-
method: 'POST',
|
|
350
|
-
body: formData,
|
|
351
|
-
credentials: 'include'
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
throw new Error('File upload failed');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const data = await response.json();
|
|
359
|
-
const { Id, Url } = data;
|
|
360
|
-
|
|
361
|
-
success(Url, Id, data);
|
|
362
|
-
} catch (err) {
|
|
363
|
-
if (error) {
|
|
364
|
-
if (err instanceof Error) {
|
|
365
|
-
error(err);
|
|
366
|
-
} else {
|
|
367
|
-
error(new Error('An unknown error occurred'));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
```
|
|
373
|
-
</details>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<details>
|
|
377
|
-
<summary>
|
|
378
|
-
🤖 AI
|
|
379
|
-
</summary>
|
|
380
|
-
|
|
381
|
-
## Connect AI
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
```
|
|
385
|
-
const fetchPromptResult = async (
|
|
386
|
-
prompt: string,
|
|
387
|
-
success: (data: string) => void,
|
|
388
|
-
error?: () => void,
|
|
389
|
-
) => {
|
|
390
|
-
try {
|
|
391
|
-
const response = await fetch(
|
|
392
|
-
'https://domain/api/v1/openai/call-any-prompt',
|
|
393
|
-
{
|
|
394
|
-
method: 'POST',
|
|
395
|
-
headers: {
|
|
396
|
-
'Content-Type': 'application/json',
|
|
397
|
-
|
|
398
|
-
Authorization: 'token',
|
|
399
|
-
},
|
|
400
|
-
body: JSON.stringify({ prompt }),
|
|
401
|
-
},
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
if (!response.ok) {
|
|
405
|
-
const errText = await response.text();
|
|
406
|
-
console.error('server error', errText);
|
|
407
|
-
throw new Error('API failed');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const data = await response.json();
|
|
411
|
-
|
|
412
|
-
success(data.content);
|
|
413
|
-
} catch (err) {
|
|
414
|
-
if (error) {
|
|
415
|
-
error();
|
|
416
|
-
console.error('Error');
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
<Editor
|
|
422
|
-
...props
|
|
423
|
-
fetchPromptResult={fetchPromptResult}
|
|
424
|
-
/>
|
|
425
|
-
```
|
|
426
|
-
</details>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
<details>
|
|
430
|
-
<summary>
|
|
431
|
-
📊 Analytics (onTrack)
|
|
432
|
-
</summary>
|
|
433
|
-
|
|
434
|
-
## Event Tracking
|
|
435
|
-
|
|
436
|
-
The editor can track user actions via the `onTrack` callback. This allows the host application to send analytics events to any provider (Yandex.Metrika, Google Analytics, Mixpanel, etc.) without the editor depending on any specific analytics SDK.
|
|
437
|
-
|
|
438
|
-
### Basic Usage
|
|
439
|
-
|
|
440
|
-
```tsx
|
|
441
|
-
import { trackGoal } from '@layers/hooks/useAnalytics';
|
|
442
|
-
|
|
443
|
-
<Editor
|
|
444
|
-
{...props}
|
|
445
|
-
onTrack={trackGoal}
|
|
446
|
-
/>
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Custom Handler
|
|
450
|
-
|
|
451
|
-
```tsx
|
|
452
|
-
const handleTrack = (event: string, params?: Record<string, unknown>) => {
|
|
453
|
-
console.log('Editor event:', event, params);
|
|
454
|
-
|
|
455
|
-
// Send to your analytics provider
|
|
456
|
-
myAnalytics.track(event, params);
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
<Editor
|
|
460
|
-
{...props}
|
|
461
|
-
onTrack={handleTrack}
|
|
462
|
-
/>
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
### Type Signature
|
|
466
|
-
|
|
467
|
-
```ts
|
|
468
|
-
type EditorTrackFn = (
|
|
469
|
-
event: string,
|
|
470
|
-
params?: Record<string, unknown>,
|
|
471
|
-
) => void;
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### Tracked Events
|
|
475
|
-
|
|
476
|
-
| Event | Trigger | Plugin |
|
|
477
|
-
|-------|---------|--------|
|
|
478
|
-
| `block_table_created` | User inserts a table | BlockFormatDropDown |
|
|
479
|
-
| `block_image_added` | User inserts an image | BlockFormatDropDown |
|
|
480
|
-
| `block_code_created` | User inserts a code block | BlockFormatDropDown |
|
|
481
|
-
| `block_layout_used` | User inserts a grid/layout | BlockFormatDropDown |
|
|
482
|
-
| `block_collapse_created` | User inserts a toggle/collapsible | BlockFormatDropDown |
|
|
483
|
-
| `block_link_inserted` | User inserts a link | ToolbarPlugin |
|
|
484
|
-
| `block_comment_added` | User adds an inline comment | ToolbarPlugin |
|
|
485
|
-
| `ai_menu_opened` | User clicks the AI toolbar button | ToolbarPlugin/AI |
|
|
486
|
-
| `block_heading_created` | User creates a heading (h1-h4) | BlockFormatDropDown |
|
|
487
|
-
| `list_bullet_created` | User creates a bullet list | BlockFormatDropDown |
|
|
488
|
-
| `list_number_created` | User creates a numbered list | BlockFormatDropDown |
|
|
489
|
-
| `list_check_created` | User creates a checklist | BlockFormatDropDown |
|
|
490
|
-
| `block_quote_created` | User creates a quote block | BlockFormatDropDown |
|
|
491
|
-
| `block_divider_inserted` | User inserts a horizontal rule | BlockFormatDropDown |
|
|
492
|
-
| `block_child_docs_inserted` | User inserts child documents block | BlockFormatDropDown |
|
|
493
|
-
| `block_embed_added` | User adds an embed/integration | BlockFormatDropDown |
|
|
494
|
-
| `media_audio_added` | User adds an audio block | BlockFormatDropDown |
|
|
495
|
-
| `media_file_added` | User adds a file block | BlockFormatDropDown |
|
|
496
|
-
| `table_row_added` | User inserts a table row | TableActionMenuPlugin |
|
|
497
|
-
| `table_column_added` | User inserts a table column | TableActionMenuPlugin |
|
|
498
|
-
| `table_row_deleted` | User deletes a table row | TableActionMenuPlugin |
|
|
499
|
-
| `table_column_deleted` | User deletes a table column | TableActionMenuPlugin |
|
|
500
|
-
| `block_duplicated` | User duplicates a block | DraggableBlockPlugin |
|
|
501
|
-
| `block_deleted` | User deletes a block | DraggableBlockPlugin |
|
|
502
|
-
| `text_code_toggled` | User toggles inline code format | ToolbarPlugin |
|
|
503
|
-
| `text_formatting_cleared` | User clears all text formatting | ToolbarPlugin |
|
|
504
|
-
|
|
505
|
-
> Events fire from both the toolbar dropdown menu and the slash command menu (`/`).
|
|
506
|
-
|
|
507
|
-
### What Happens Without onTrack?
|
|
508
|
-
|
|
509
|
-
If `onTrack` is not provided, **nothing happens** — all tracking calls use optional chaining (`onTrack?.('event')`) and are silently skipped. The editor works exactly the same with or without analytics. There is no error, no console warning, and no performance impact.
|
|
510
|
-
|
|
511
|
-
This means:
|
|
512
|
-
- **No analytics SDK required** — the editor is a standalone package with zero analytics dependencies
|
|
513
|
-
- **Safe to omit** — if you don't need tracking, simply don't pass the prop
|
|
514
|
-
- **No counter needed** — the editor itself never calls `ym()`, `gtag()`, or any external API directly
|
|
515
|
-
|
|
516
|
-
### How It Works Internally
|
|
517
|
-
|
|
518
|
-
1. `onTrack` is passed as a prop to `<Editor>` and stored in React Context
|
|
519
|
-
2. Plugins access it via `useContext(Context)` and call `onTrack?.('event_name')`
|
|
520
|
-
3. The host app decides what to do with the event (send to Metrika, log, ignore)
|
|
521
|
-
|
|
522
|
-
```
|
|
523
|
-
Host App Editor Package
|
|
524
|
-
───────── ──────────────
|
|
525
|
-
trackGoal() ──→ onTrack prop ──→ Context.onTrack
|
|
526
|
-
│ │
|
|
527
|
-
│ Plugins call:
|
|
528
|
-
│ onTrack?.('block_table_created')
|
|
529
|
-
│ │
|
|
530
|
-
◄────────────────────────────────┘
|
|
531
|
-
│
|
|
532
|
-
ym(ID, 'reachGoal', 'block_table_created')
|
|
533
|
-
dataLayer.push({ event: 'block_table_created' })
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
</details>
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
<details>
|
|
540
|
-
<summary>
|
|
541
|
-
👥 Collaboration
|
|
542
|
-
</summary>
|
|
543
|
-
|
|
544
|
-
```jsx
|
|
545
|
-
<Editor
|
|
546
|
-
{...props}
|
|
547
|
-
ws={{
|
|
548
|
-
url: 'https://wss.dudoc.io/', // WebSocket URL
|
|
549
|
-
id: '322323', // Unique document ID
|
|
550
|
-
user: userProfile, // Current user
|
|
551
|
-
getActiveUsers: (users) => {
|
|
552
|
-
// Returns active users editing the document
|
|
553
|
-
setActiveUsers(users);
|
|
554
|
-
},
|
|
555
|
-
}}
|
|
556
|
-
/>
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
</details>
|
|
560
|
-
|
|
561
|
-
<details>
|
|
562
|
-
<summary>
|
|
563
|
-
📝 Additional options
|
|
564
|
-
</summary>
|
|
565
|
-
|
|
566
|
-
## Reset editor content
|
|
567
|
-
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
import { CLEAR_EDITOR_COMMAND } from './EditorLexical';
|
|
571
|
-
|
|
572
|
-
<>
|
|
573
|
-
<button
|
|
574
|
-
onClick={() => {
|
|
575
|
-
if (editorRef.current) {
|
|
576
|
-
editorRef.current.update(() => {
|
|
577
|
-
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}}
|
|
581
|
-
>
|
|
582
|
-
Reset
|
|
583
|
-
</button>
|
|
584
|
-
<Editor
|
|
585
|
-
...props
|
|
586
|
-
editorRef={editorRef}
|
|
587
|
-
/>
|
|
588
|
-
<>
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
</details>
|
|
592
|
-
|
|
593
|
-
<details>
|
|
594
|
-
<summary>
|
|
595
|
-
🧪 Testing
|
|
596
|
-
</summary>
|
|
597
|
-
|
|
598
|
-
## Testing Overview
|
|
599
|
-
|
|
600
|
-
This project includes comprehensive testing with both **unit tests** (Vitest) and **end-to-end tests** (Playwright). The testing setup ensures reliability across different browsers and environments.
|
|
601
|
-
|
|
602
|
-
### Prerequisites
|
|
603
|
-
|
|
604
|
-
Before running tests, make sure you have installed all dependencies:
|
|
605
|
-
|
|
606
|
-
```bash
|
|
607
|
-
npm install
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
## Unit Tests (Vitest)
|
|
611
|
-
|
|
612
|
-
Unit tests are written with **Vitest** and **jsdom** for testing individual components and utilities.
|
|
613
|
-
|
|
614
|
-
### Run Unit Tests
|
|
615
|
-
|
|
616
|
-
```bash
|
|
617
|
-
# Run all unit tests
|
|
618
|
-
npm run test-unit
|
|
619
|
-
|
|
620
|
-
# Run unit tests in watch mode (auto-rerun on changes)
|
|
621
|
-
npm run test-unit-watch
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
### Unit Test Files Location
|
|
625
|
-
- `__tests__/unit/` - Unit test files
|
|
626
|
-
- Test files follow the pattern: `*.test.ts` or `*.test.tsx`
|
|
627
|
-
|
|
628
|
-
## End-to-End Tests (Playwright)
|
|
629
|
-
|
|
630
|
-
E2E tests use **Playwright** to test the complete application flow in real browsers.
|
|
631
|
-
|
|
632
|
-
### Run E2E Tests
|
|
633
|
-
|
|
634
|
-
```bash
|
|
635
|
-
# Run all E2E tests (WebKit only for CI optimization)
|
|
636
|
-
npm run test:e2e
|
|
637
|
-
|
|
638
|
-
# Run E2E tests with UI mode (interactive)
|
|
639
|
-
npm run test:e2e:ui
|
|
640
|
-
|
|
641
|
-
# Run E2E tests in debug mode
|
|
642
|
-
npm run test:e2e:debug
|
|
643
|
-
|
|
644
|
-
# Run E2E tests in headed mode (visible browser)
|
|
645
|
-
npm run test:e2e:headed
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
### E2E Test Files Location
|
|
649
|
-
- `__tests__/e2e/` - End-to-end test files
|
|
650
|
-
- `__tests__/regression/` - Regression test files
|
|
651
|
-
- Test files follow the pattern: `*.spec.js`, `*.spec.mjs`, or `*.spec.ts`
|
|
652
|
-
|
|
653
|
-
### Browser Support
|
|
654
|
-
- **WebKit** (Safari) - Primary browser for CI/CD
|
|
655
|
-
- **Chromium** and **Firefox** - Available for local testing
|
|
656
|
-
|
|
657
|
-
## Test Server
|
|
658
|
-
|
|
659
|
-
The test server automatically starts when running E2E tests:
|
|
660
|
-
|
|
661
|
-
```bash
|
|
662
|
-
# Manual test server start (if needed)
|
|
663
|
-
npm run start-test-server
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
- **URL**: `http://localhost:3000`
|
|
667
|
-
- **Mode**: Full editor mode with all features enabled
|
|
668
|
-
- **Environment**: `VITE_LAYERS=true`
|
|
669
|
-
|
|
670
|
-
## Test Configuration
|
|
671
|
-
|
|
672
|
-
### Playwright Configuration
|
|
673
|
-
- **Config file**: `playwright.config.js`
|
|
674
|
-
- **Test directory**: `./__tests__/e2e/`
|
|
675
|
-
- **Browser**: WebKit (optimized for CI)
|
|
676
|
-
- **Base URL**: `http://localhost:3000`
|
|
677
|
-
- **Timeout**: 90 seconds per test
|
|
678
|
-
- **Retries**: 2 retries in CI, 0 locally
|
|
679
|
-
|
|
680
|
-
### Vitest Configuration
|
|
681
|
-
- **Config file**: `vitest.config.mts`
|
|
682
|
-
- **Environment**: jsdom
|
|
683
|
-
- **Setup file**: `vitest.setup.mts`
|
|
684
|
-
- **Coverage**: V8 provider
|
|
685
|
-
|
|
686
|
-
## CI/CD Testing
|
|
687
|
-
|
|
688
|
-
Tests run automatically on:
|
|
689
|
-
- **Push** to `main` or `dev` branches
|
|
690
|
-
- **Pull requests** to `main` or `dev` branches
|
|
691
|
-
- **Manual trigger** via GitHub Actions
|
|
692
|
-
|
|
693
|
-
### GitHub Actions Workflow
|
|
694
|
-
- **File**: `.github/workflows/tests.yml`
|
|
695
|
-
- **Runner**: Ubuntu Latest
|
|
696
|
-
- **Node.js**: Version 20
|
|
697
|
-
- **Browser caching**: Playwright browsers cached for faster runs
|
|
698
|
-
- **Artifacts**: Test reports and traces uploaded on completion
|
|
699
|
-
|
|
700
|
-
## Test Examples
|
|
701
|
-
|
|
702
|
-
### Basic E2E Test Structure
|
|
703
|
-
|
|
704
|
-
```javascript
|
|
705
|
-
// __tests__/e2e/example.spec.mjs
|
|
706
|
-
import { test, expect } from '@playwright/test';
|
|
707
|
-
import { focusEditor } from '../utils/index.mjs';
|
|
708
|
-
|
|
709
|
-
test('Can type text in editor', async ({ page }) => {
|
|
710
|
-
await page.goto('/');
|
|
711
|
-
await focusEditor(page);
|
|
712
|
-
|
|
713
|
-
const editor = page.locator('[contenteditable="true"]').first();
|
|
714
|
-
await editor.type('Hello World');
|
|
715
|
-
|
|
716
|
-
await expect(editor).toContainText('Hello World');
|
|
717
|
-
});
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### Unit Test Structure
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
// __tests__/unit/example.test.ts
|
|
724
|
-
import { describe, it, expect } from 'vitest';
|
|
725
|
-
import { render } from '@testing-library/react';
|
|
726
|
-
import { Editor } from '../src/Editor';
|
|
727
|
-
|
|
728
|
-
describe('Editor Component', () => {
|
|
729
|
-
it('renders without crashing', () => {
|
|
730
|
-
const { container } = render(<Editor />);
|
|
731
|
-
expect(container).toBeTruthy();
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
## Debugging Tests
|
|
737
|
-
|
|
738
|
-
### Debug E2E Tests
|
|
739
|
-
|
|
740
|
-
```bash
|
|
741
|
-
# Run with Playwright Inspector
|
|
742
|
-
npm run test:e2e:debug
|
|
743
|
-
|
|
744
|
-
# Run specific test file
|
|
745
|
-
npx playwright test __tests__/e2e/TextEntry.spec.mjs --debug
|
|
746
|
-
|
|
747
|
-
# Run with headed browser
|
|
748
|
-
npm run test:e2e:headed
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
### View Test Reports
|
|
752
|
-
|
|
753
|
-
```bash
|
|
754
|
-
# Open HTML report (after running tests)
|
|
755
|
-
npx playwright show-report
|
|
756
|
-
|
|
757
|
-
# View test traces (for failed tests)
|
|
758
|
-
npx playwright show-trace test-results/[test-name]/trace.zip
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
## Test Utilities
|
|
762
|
-
|
|
763
|
-
Common test utilities are available in `__tests__/utils/index.mjs`:
|
|
764
|
-
|
|
765
|
-
- `focusEditor(page)` - Focus the main editor
|
|
766
|
-
- `selectAll(page)` - Select all text in editor
|
|
767
|
-
- `moveLeft(page, count)` - Move cursor left
|
|
768
|
-
- `selectCharacters(page, count)` - Select specific number of characters
|
|
769
|
-
- `waitForSelector(page, selector)` - Wait for element to appear
|
|
770
|
-
|
|
771
|
-
## Performance
|
|
772
|
-
|
|
773
|
-
### Test Execution Times
|
|
774
|
-
- **Unit Tests**: ~10-30 seconds
|
|
775
|
-
- **E2E Tests (first run)**: ~3-4 minutes (includes browser installation)
|
|
776
|
-
- **E2E Tests (cached)**: ~1-2 minutes (uses cached browsers)
|
|
777
|
-
|
|
778
|
-
### Optimization Features
|
|
779
|
-
- **Browser Caching**: Playwright browsers cached in CI
|
|
780
|
-
- **Single Worker**: Prevents race conditions in CI
|
|
781
|
-
- **WebKit Only**: Faster than multi-browser matrix
|
|
782
|
-
- **Smart Retries**: Auto-retry flaky tests
|
|
783
|
-
|
|
784
|
-
## Troubleshooting
|
|
785
|
-
|
|
786
|
-
### Common Issues
|
|
787
|
-
|
|
788
|
-
1. **Port conflicts**: Ensure port 3000 is available
|
|
789
|
-
2. **Browser installation**: Run `npx playwright install` if needed
|
|
790
|
-
3. **Test timeouts**: Check if test server is running properly
|
|
791
|
-
4. **Certificate errors**: Tests use HTTP to avoid HTTPS certificate issues
|
|
792
|
-
|
|
793
|
-
### Reset Test Environment
|
|
794
|
-
|
|
795
|
-
```bash
|
|
796
|
-
# Clear Playwright cache
|
|
797
|
-
npx playwright install --force
|
|
798
|
-
|
|
799
|
-
# Reset node_modules
|
|
800
|
-
rm -rf node_modules package-lock.json
|
|
801
|
-
npm install
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
</details>
|
|
805
|
-
|
|
806
|
-
<details>
|
|
807
|
-
<summary>
|
|
808
|
-
⚙️ Properties
|
|
809
|
-
</summary>
|
|
810
|
-
|
|
811
|
-
```
|
|
812
|
-
onChange: (value: string | object) => undefined - A function that triggers every time the editor content changes and returns an HTML string or an object depending on the outputFormat property.
|
|
813
|
-
debounce?: number - Defines how often the onChange function is called, in milliseconds.
|
|
814
|
-
onBlur: (value: string | object) => undefined - A function that triggers when the editor loses focus and returns an HTML string or an object depending on the outputFormat property.
|
|
815
|
-
outputFormat?: 'html' | 'json' - The outputFormat property defines whether we want to output an HTML string or a JSON object. The default is JSON.
|
|
816
|
-
initialContent: string | object - The initial content for the editor.
|
|
817
|
-
maxHeight?: number - Sets the height of the editor. The default is 100%.
|
|
818
|
-
mode?: 'simple' | 'default' | 'full' | 'editor' - The editor mode. Depending on the chosen mode, functionality may be restricted or extended. The default is default.
|
|
819
|
-
fetchUploadMedia?: (file: File, success: (url: string, id: string, error?: (error?: Error) => void) => void) - Function to upload an image to your service.
|
|
820
|
-
fetchDeleteMedia?: (id: string, success: () => void, error?: (error?: Error) => void) - Helper function to delete an image.
|
|
821
|
-
maxFileSize?: number - The maximum image size in megabytes.
|
|
822
|
-
contentModalUploadImage?: React.FunctionComponent - A React component to replace content in DropZone.
|
|
823
|
-
maxImageSizeError?: () => void - A function that is called if the image exceeds the maxFileSize.
|
|
824
|
-
disable?: boolean - Toggles the editor into read-only mode.
|
|
825
|
-
ws?: { url: string, id: string, user: { color: string, name: string }, getActiveUsers: (users) => void } - WebSocket settings: URL, document ID, current user details, and function to return active users editing the document.
|
|
826
|
-
editorRef?: { current: EditorType | null } - Reference to the editor.
|
|
827
|
-
onTrack?: (event: string, params?: Record<string, unknown>) => void - Optional callback for analytics event tracking. Called when the user performs tracked actions (insert table, add image, open AI menu, etc.). If not provided, tracking is silently skipped. See the "Analytics (onTrack)" section for the full list of events.
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
</details>
|
|
1
|
+
# LayersTextEditor
|
|
2
|
+
|
|
3
|
+
LayersTextEditor is a text editor for web applications written in JavaScript, with a focus on reliability, accessibility, and performance.
|
|
4
|
+
|
|
5
|
+
<details>
|
|
6
|
+
<summary>
|
|
7
|
+
🚀 Quick Start
|
|
8
|
+
</summary>
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
To install the package, run one of the following commands:
|
|
13
|
+
|
|
14
|
+
### Use npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @layers-app/editor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> If you plan to use the Swagger node, install the additional package: `npm install swagger-ui-react`.
|
|
21
|
+
|
|
22
|
+
### Use yarn:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
yarn add @layers-app/editor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Initializing the text editor
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
import { Editor } from '@layers-app/editor';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
By default, LayersTextEditor works with an object and can return either an object or HTML.
|
|
35
|
+
|
|
36
|
+
Example with an object:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const text = 'Hello world';
|
|
40
|
+
|
|
41
|
+
const json = {
|
|
42
|
+
root: {
|
|
43
|
+
children: [
|
|
44
|
+
{
|
|
45
|
+
children: [
|
|
46
|
+
{
|
|
47
|
+
detail: 0,
|
|
48
|
+
format: 0,
|
|
49
|
+
mode: 'normal',
|
|
50
|
+
style: '',
|
|
51
|
+
text: text,
|
|
52
|
+
type: 'text',
|
|
53
|
+
version: 1,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
direction: 'ltr',
|
|
57
|
+
format: '',
|
|
58
|
+
indent: 0,
|
|
59
|
+
type: 'paragraph',
|
|
60
|
+
version: 1,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
direction: 'ltr',
|
|
64
|
+
format: '',
|
|
65
|
+
indent: 0,
|
|
66
|
+
type: 'root',
|
|
67
|
+
version: 1,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onChange = (
|
|
72
|
+
data, // json
|
|
73
|
+
) => <Editor initialContent={json} onChange={onChange} />;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can also pass an HTML string to the editor.
|
|
77
|
+
|
|
78
|
+
Example with HTML:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
const html = `
|
|
82
|
+
<h2 dir="ltr" style="text-align: left;">
|
|
83
|
+
<span style="background-color: rgb(248, 231, 28); font-family: "Trebuchet MS"; white-space: pre-wrap;">Hello</span>
|
|
84
|
+
</h2>
|
|
85
|
+
<h2 dir="ltr">
|
|
86
|
+
<br>
|
|
87
|
+
</h2>
|
|
88
|
+
<p dir="ltr">
|
|
89
|
+
<br>
|
|
90
|
+
</p>
|
|
91
|
+
<p dir="ltr">
|
|
92
|
+
<span style="font-size: 21px; white-space: pre-wrap;">world</span>
|
|
93
|
+
</p>
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
const onChange = (data) => // json
|
|
97
|
+
|
|
98
|
+
<Editor initialContent={html} onChange={onChange} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The output of the data in the `onChange` function is controlled by the **outputFormat** property. **outputFormat** can be either "html" or "json". Example with **outputFormat**:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
const html = `
|
|
105
|
+
<h2 dir="ltr" style="text-align: left;">
|
|
106
|
+
<span style="background-color: rgb(248, 231, 28); font-family: "Trebuchet MS"; white-space: pre-wrap;">Hello</span>
|
|
107
|
+
</h2>
|
|
108
|
+
<h2 dir="ltr">
|
|
109
|
+
<br>
|
|
110
|
+
</h2>
|
|
111
|
+
<p dir="ltr">
|
|
112
|
+
<br>
|
|
113
|
+
</p>
|
|
114
|
+
<p dir="ltr">
|
|
115
|
+
<span style="font-size: 21px; white-space: pre-wrap;">world</span>
|
|
116
|
+
</p>
|
|
117
|
+
`
|
|
118
|
+
|
|
119
|
+
const onChange = (data: string, text?: string) => {
|
|
120
|
+
// data - html from editor
|
|
121
|
+
// text - text from editor
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
<Editor initialContent={html} outputFormat="html" onChange={onChange} />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary>
|
|
132
|
+
🎨 StylesProvider
|
|
133
|
+
</summary>
|
|
134
|
+
|
|
135
|
+
Use **StylesProvider** to add styling to your HTML content.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
<StylesProvider>
|
|
139
|
+
<div
|
|
140
|
+
dangerouslySetInnerHTML={{ __html: '<p>Your html here</p>' }}
|
|
141
|
+
/>
|
|
142
|
+
</StylesProvider>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
<details>
|
|
148
|
+
<summary>
|
|
149
|
+
🖼️ File upload
|
|
150
|
+
</summary>
|
|
151
|
+
|
|
152
|
+
## Image upload
|
|
153
|
+
|
|
154
|
+
To start working with image uploads, use the **fetchUploadMedia** function, which takes three parameters: **file**, **success**, and **error**. After successfully uploading the image to your service, you should call the **success** function and pass two required arguments: the **URL** of the image and its **ID**.
|
|
155
|
+
Optional: You can also pass two optional parameters: **signal** and **onProgress**. The **signal** allows you to cancel an ongoing upload using an AbortController, and **onProgress** provides the current upload progress in percent — useful for displaying a progress bar or loading state.
|
|
156
|
+
```
|
|
157
|
+
const fetchUploadMedia = async (
|
|
158
|
+
file: File,
|
|
159
|
+
success: (url: string, id: string) => void,
|
|
160
|
+
error?: (error?: Error) => void
|
|
161
|
+
) => {
|
|
162
|
+
const formData = new FormData();
|
|
163
|
+
formData.append('File', file);
|
|
164
|
+
formData.append('FileAccessModifier', '0');
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch('/api/v1/Files/Upload', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: formData,
|
|
170
|
+
credentials: 'include'
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error('File upload failed');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
const { Id, Url } = data;
|
|
179
|
+
|
|
180
|
+
success(Url, Id);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (error) {
|
|
183
|
+
if (err instanceof Error) {
|
|
184
|
+
error(err);
|
|
185
|
+
} else {
|
|
186
|
+
error(new Error('An unknown error occurred'));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const fetchUploadMedia = async (
|
|
193
|
+
file: File,
|
|
194
|
+
success: (url: string, id: string, data: any) => void,
|
|
195
|
+
error?: (err: Error) => void,
|
|
196
|
+
signal?: AbortSignal,
|
|
197
|
+
onProgress?: (percent: number) => void,
|
|
198
|
+
) => {
|
|
199
|
+
const formData = new FormData();
|
|
200
|
+
formData.append('File', file);
|
|
201
|
+
formData.append('FileAccessModifier', '0');
|
|
202
|
+
|
|
203
|
+
const xhr = new XMLHttpRequest();
|
|
204
|
+
xhr.open('POST', '/api/v1/Files/Upload');
|
|
205
|
+
xhr.withCredentials = true;
|
|
206
|
+
|
|
207
|
+
if (signal) signal.addEventListener('abort', () => xhr.abort());
|
|
208
|
+
if (onProgress) {
|
|
209
|
+
xhr.upload.onprogress = (e) => {
|
|
210
|
+
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
xhr.onload = () => {
|
|
215
|
+
try {
|
|
216
|
+
const data = JSON.parse(xhr.responseText);
|
|
217
|
+
success(`/v1/attachments/${data.id}`, data.id, data);
|
|
218
|
+
} catch {
|
|
219
|
+
error?.(new Error('Invalid response'));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
xhr.onerror = () => error?.(new Error('Upload error')));
|
|
224
|
+
xhr.send(formData);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
<Editor
|
|
229
|
+
...props
|
|
230
|
+
fetchUploadMedia={fetchUploadMedia}
|
|
231
|
+
/>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Image Deletion
|
|
235
|
+
|
|
236
|
+
To have greater control over image deletion, pass an optional function **fetchDeleteMedia** to the editor, which accepts three parameters: **id**, **success**, and **error**. After successfully deleting the image from your service, the **success** function should be called.
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
const fetchDeleteMedia = async (
|
|
240
|
+
id: string,
|
|
241
|
+
success: () => void,
|
|
242
|
+
error?: (error?: Error) => void
|
|
243
|
+
) => {
|
|
244
|
+
const body = { Ids: [id] };
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const response = await fetch('/api/v1/Documents/Delete', {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers: {
|
|
250
|
+
'Content-Type': 'application/json'
|
|
251
|
+
},
|
|
252
|
+
body: JSON.stringify(body),
|
|
253
|
+
credentials: 'include'
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await response.json();
|
|
257
|
+
success();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (error) {
|
|
260
|
+
if (err instanceof Error) {
|
|
261
|
+
error(err);
|
|
262
|
+
} else {
|
|
263
|
+
error(new Error('An unknown error occurred'));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
<Editor
|
|
270
|
+
...props
|
|
271
|
+
fetchUploadMedia={fetchUploadMedia}
|
|
272
|
+
fetchDeleteMedia={fetchUploadMedia}
|
|
273
|
+
/>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Additional options for working with image uploads.
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
import { Editor, Dropzone } from "@sinups/editor-dsd";
|
|
280
|
+
|
|
281
|
+
const Content = () => (
|
|
282
|
+
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
|
283
|
+
{/*
|
|
284
|
+
The components Dropzone.Accept, Dropzone.Reject, and Dropzone.Idle are visible only when the user performs specific actions:
|
|
285
|
+
|
|
286
|
+
Dropzone.Accept is visible only when the user drags a file that can be accepted into the drop zone.
|
|
287
|
+
Dropzone.Reject is visible only when the user drags a file that cannot be accepted into the drop zone.
|
|
288
|
+
Dropzone.Idle is visible when the user is not dragging any file into the drop zone.
|
|
289
|
+
*/}
|
|
290
|
+
<Dropzone.Accept>
|
|
291
|
+
<IconUpload
|
|
292
|
+
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)' }}
|
|
293
|
+
stroke={1.5}
|
|
294
|
+
/>
|
|
295
|
+
</Dropzone.Accept>
|
|
296
|
+
<Dropzone.Reject>
|
|
297
|
+
<IconX
|
|
298
|
+
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)' }}
|
|
299
|
+
stroke={1.5}
|
|
300
|
+
/>
|
|
301
|
+
</Dropzone.Reject>
|
|
302
|
+
<Dropzone.Idle>
|
|
303
|
+
<IconPhoto
|
|
304
|
+
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)' }}
|
|
305
|
+
stroke={1.5}
|
|
306
|
+
/>
|
|
307
|
+
</Dropzone.Idle>
|
|
308
|
+
|
|
309
|
+
<div>
|
|
310
|
+
<Text size="xl" inline>
|
|
311
|
+
Drag images here or click to select files
|
|
312
|
+
</Text>
|
|
313
|
+
<Text size="sm" c="dimmed" inline mt={7}>
|
|
314
|
+
Attach as many files as you want, each file must not exceed{' '} {maxFileSize} МБ.
|
|
315
|
+
</Text>
|
|
316
|
+
</div>
|
|
317
|
+
</Group>
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
<Editor
|
|
321
|
+
...props
|
|
322
|
+
fetchUploadMedia={fetchUploadMedia}
|
|
323
|
+
contentModalUploadImage={Content}
|
|
324
|
+
maxFileSize={5}
|
|
325
|
+
maxImageSizeError={() => {}}
|
|
326
|
+
/>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## File upload
|
|
330
|
+
|
|
331
|
+
For uploading a file or audio, you might need the third parameter "data".
|
|
332
|
+
|
|
333
|
+
```
|
|
334
|
+
const fetchUploadMedia = async (
|
|
335
|
+
file: File,
|
|
336
|
+
success: (url: string, id: string, data?: {
|
|
337
|
+
contentType: string;
|
|
338
|
+
fileSize: string;
|
|
339
|
+
originalFileName: string;
|
|
340
|
+
}) => void,
|
|
341
|
+
error?: (error?: Error) => void
|
|
342
|
+
) => {
|
|
343
|
+
const formData = new FormData();
|
|
344
|
+
formData.append('File', file);
|
|
345
|
+
formData.append('FileAccessModifier', '0');
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const response = await fetch('/api/v1/Files/Upload', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
body: formData,
|
|
351
|
+
credentials: 'include'
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error('File upload failed');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
const { Id, Url } = data;
|
|
360
|
+
|
|
361
|
+
success(Url, Id, data);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (error) {
|
|
364
|
+
if (err instanceof Error) {
|
|
365
|
+
error(err);
|
|
366
|
+
} else {
|
|
367
|
+
error(new Error('An unknown error occurred'));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
```
|
|
373
|
+
</details>
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
<details>
|
|
377
|
+
<summary>
|
|
378
|
+
🤖 AI
|
|
379
|
+
</summary>
|
|
380
|
+
|
|
381
|
+
## Connect AI
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
const fetchPromptResult = async (
|
|
386
|
+
prompt: string,
|
|
387
|
+
success: (data: string) => void,
|
|
388
|
+
error?: () => void,
|
|
389
|
+
) => {
|
|
390
|
+
try {
|
|
391
|
+
const response = await fetch(
|
|
392
|
+
'https://domain/api/v1/openai/call-any-prompt',
|
|
393
|
+
{
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: {
|
|
396
|
+
'Content-Type': 'application/json',
|
|
397
|
+
|
|
398
|
+
Authorization: 'token',
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({ prompt }),
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const errText = await response.text();
|
|
406
|
+
console.error('server error', errText);
|
|
407
|
+
throw new Error('API failed');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const data = await response.json();
|
|
411
|
+
|
|
412
|
+
success(data.content);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (error) {
|
|
415
|
+
error();
|
|
416
|
+
console.error('Error');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
<Editor
|
|
422
|
+
...props
|
|
423
|
+
fetchPromptResult={fetchPromptResult}
|
|
424
|
+
/>
|
|
425
|
+
```
|
|
426
|
+
</details>
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
<details>
|
|
430
|
+
<summary>
|
|
431
|
+
📊 Analytics (onTrack)
|
|
432
|
+
</summary>
|
|
433
|
+
|
|
434
|
+
## Event Tracking
|
|
435
|
+
|
|
436
|
+
The editor can track user actions via the `onTrack` callback. This allows the host application to send analytics events to any provider (Yandex.Metrika, Google Analytics, Mixpanel, etc.) without the editor depending on any specific analytics SDK.
|
|
437
|
+
|
|
438
|
+
### Basic Usage
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
import { trackGoal } from '@layers/hooks/useAnalytics';
|
|
442
|
+
|
|
443
|
+
<Editor
|
|
444
|
+
{...props}
|
|
445
|
+
onTrack={trackGoal}
|
|
446
|
+
/>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Custom Handler
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
const handleTrack = (event: string, params?: Record<string, unknown>) => {
|
|
453
|
+
console.log('Editor event:', event, params);
|
|
454
|
+
|
|
455
|
+
// Send to your analytics provider
|
|
456
|
+
myAnalytics.track(event, params);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
<Editor
|
|
460
|
+
{...props}
|
|
461
|
+
onTrack={handleTrack}
|
|
462
|
+
/>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Type Signature
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
type EditorTrackFn = (
|
|
469
|
+
event: string,
|
|
470
|
+
params?: Record<string, unknown>,
|
|
471
|
+
) => void;
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Tracked Events
|
|
475
|
+
|
|
476
|
+
| Event | Trigger | Plugin |
|
|
477
|
+
|-------|---------|--------|
|
|
478
|
+
| `block_table_created` | User inserts a table | BlockFormatDropDown |
|
|
479
|
+
| `block_image_added` | User inserts an image | BlockFormatDropDown |
|
|
480
|
+
| `block_code_created` | User inserts a code block | BlockFormatDropDown |
|
|
481
|
+
| `block_layout_used` | User inserts a grid/layout | BlockFormatDropDown |
|
|
482
|
+
| `block_collapse_created` | User inserts a toggle/collapsible | BlockFormatDropDown |
|
|
483
|
+
| `block_link_inserted` | User inserts a link | ToolbarPlugin |
|
|
484
|
+
| `block_comment_added` | User adds an inline comment | ToolbarPlugin |
|
|
485
|
+
| `ai_menu_opened` | User clicks the AI toolbar button | ToolbarPlugin/AI |
|
|
486
|
+
| `block_heading_created` | User creates a heading (h1-h4) | BlockFormatDropDown |
|
|
487
|
+
| `list_bullet_created` | User creates a bullet list | BlockFormatDropDown |
|
|
488
|
+
| `list_number_created` | User creates a numbered list | BlockFormatDropDown |
|
|
489
|
+
| `list_check_created` | User creates a checklist | BlockFormatDropDown |
|
|
490
|
+
| `block_quote_created` | User creates a quote block | BlockFormatDropDown |
|
|
491
|
+
| `block_divider_inserted` | User inserts a horizontal rule | BlockFormatDropDown |
|
|
492
|
+
| `block_child_docs_inserted` | User inserts child documents block | BlockFormatDropDown |
|
|
493
|
+
| `block_embed_added` | User adds an embed/integration | BlockFormatDropDown |
|
|
494
|
+
| `media_audio_added` | User adds an audio block | BlockFormatDropDown |
|
|
495
|
+
| `media_file_added` | User adds a file block | BlockFormatDropDown |
|
|
496
|
+
| `table_row_added` | User inserts a table row | TableActionMenuPlugin |
|
|
497
|
+
| `table_column_added` | User inserts a table column | TableActionMenuPlugin |
|
|
498
|
+
| `table_row_deleted` | User deletes a table row | TableActionMenuPlugin |
|
|
499
|
+
| `table_column_deleted` | User deletes a table column | TableActionMenuPlugin |
|
|
500
|
+
| `block_duplicated` | User duplicates a block | DraggableBlockPlugin |
|
|
501
|
+
| `block_deleted` | User deletes a block | DraggableBlockPlugin |
|
|
502
|
+
| `text_code_toggled` | User toggles inline code format | ToolbarPlugin |
|
|
503
|
+
| `text_formatting_cleared` | User clears all text formatting | ToolbarPlugin |
|
|
504
|
+
|
|
505
|
+
> Events fire from both the toolbar dropdown menu and the slash command menu (`/`).
|
|
506
|
+
|
|
507
|
+
### What Happens Without onTrack?
|
|
508
|
+
|
|
509
|
+
If `onTrack` is not provided, **nothing happens** — all tracking calls use optional chaining (`onTrack?.('event')`) and are silently skipped. The editor works exactly the same with or without analytics. There is no error, no console warning, and no performance impact.
|
|
510
|
+
|
|
511
|
+
This means:
|
|
512
|
+
- **No analytics SDK required** — the editor is a standalone package with zero analytics dependencies
|
|
513
|
+
- **Safe to omit** — if you don't need tracking, simply don't pass the prop
|
|
514
|
+
- **No counter needed** — the editor itself never calls `ym()`, `gtag()`, or any external API directly
|
|
515
|
+
|
|
516
|
+
### How It Works Internally
|
|
517
|
+
|
|
518
|
+
1. `onTrack` is passed as a prop to `<Editor>` and stored in React Context
|
|
519
|
+
2. Plugins access it via `useContext(Context)` and call `onTrack?.('event_name')`
|
|
520
|
+
3. The host app decides what to do with the event (send to Metrika, log, ignore)
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
Host App Editor Package
|
|
524
|
+
───────── ──────────────
|
|
525
|
+
trackGoal() ──→ onTrack prop ──→ Context.onTrack
|
|
526
|
+
│ │
|
|
527
|
+
│ Plugins call:
|
|
528
|
+
│ onTrack?.('block_table_created')
|
|
529
|
+
│ │
|
|
530
|
+
◄────────────────────────────────┘
|
|
531
|
+
│
|
|
532
|
+
ym(ID, 'reachGoal', 'block_table_created')
|
|
533
|
+
dataLayer.push({ event: 'block_table_created' })
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
</details>
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
<details>
|
|
540
|
+
<summary>
|
|
541
|
+
👥 Collaboration
|
|
542
|
+
</summary>
|
|
543
|
+
|
|
544
|
+
```jsx
|
|
545
|
+
<Editor
|
|
546
|
+
{...props}
|
|
547
|
+
ws={{
|
|
548
|
+
url: 'https://wss.dudoc.io/', // WebSocket URL
|
|
549
|
+
id: '322323', // Unique document ID
|
|
550
|
+
user: userProfile, // Current user
|
|
551
|
+
getActiveUsers: (users) => {
|
|
552
|
+
// Returns active users editing the document
|
|
553
|
+
setActiveUsers(users);
|
|
554
|
+
},
|
|
555
|
+
}}
|
|
556
|
+
/>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
</details>
|
|
560
|
+
|
|
561
|
+
<details>
|
|
562
|
+
<summary>
|
|
563
|
+
📝 Additional options
|
|
564
|
+
</summary>
|
|
565
|
+
|
|
566
|
+
## Reset editor content
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
import { CLEAR_EDITOR_COMMAND } from './EditorLexical';
|
|
571
|
+
|
|
572
|
+
<>
|
|
573
|
+
<button
|
|
574
|
+
onClick={() => {
|
|
575
|
+
if (editorRef.current) {
|
|
576
|
+
editorRef.current.update(() => {
|
|
577
|
+
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}}
|
|
581
|
+
>
|
|
582
|
+
Reset
|
|
583
|
+
</button>
|
|
584
|
+
<Editor
|
|
585
|
+
...props
|
|
586
|
+
editorRef={editorRef}
|
|
587
|
+
/>
|
|
588
|
+
<>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
</details>
|
|
592
|
+
|
|
593
|
+
<details>
|
|
594
|
+
<summary>
|
|
595
|
+
🧪 Testing
|
|
596
|
+
</summary>
|
|
597
|
+
|
|
598
|
+
## Testing Overview
|
|
599
|
+
|
|
600
|
+
This project includes comprehensive testing with both **unit tests** (Vitest) and **end-to-end tests** (Playwright). The testing setup ensures reliability across different browsers and environments.
|
|
601
|
+
|
|
602
|
+
### Prerequisites
|
|
603
|
+
|
|
604
|
+
Before running tests, make sure you have installed all dependencies:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
npm install
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Unit Tests (Vitest)
|
|
611
|
+
|
|
612
|
+
Unit tests are written with **Vitest** and **jsdom** for testing individual components and utilities.
|
|
613
|
+
|
|
614
|
+
### Run Unit Tests
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
# Run all unit tests
|
|
618
|
+
npm run test-unit
|
|
619
|
+
|
|
620
|
+
# Run unit tests in watch mode (auto-rerun on changes)
|
|
621
|
+
npm run test-unit-watch
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Unit Test Files Location
|
|
625
|
+
- `__tests__/unit/` - Unit test files
|
|
626
|
+
- Test files follow the pattern: `*.test.ts` or `*.test.tsx`
|
|
627
|
+
|
|
628
|
+
## End-to-End Tests (Playwright)
|
|
629
|
+
|
|
630
|
+
E2E tests use **Playwright** to test the complete application flow in real browsers.
|
|
631
|
+
|
|
632
|
+
### Run E2E Tests
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
# Run all E2E tests (WebKit only for CI optimization)
|
|
636
|
+
npm run test:e2e
|
|
637
|
+
|
|
638
|
+
# Run E2E tests with UI mode (interactive)
|
|
639
|
+
npm run test:e2e:ui
|
|
640
|
+
|
|
641
|
+
# Run E2E tests in debug mode
|
|
642
|
+
npm run test:e2e:debug
|
|
643
|
+
|
|
644
|
+
# Run E2E tests in headed mode (visible browser)
|
|
645
|
+
npm run test:e2e:headed
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### E2E Test Files Location
|
|
649
|
+
- `__tests__/e2e/` - End-to-end test files
|
|
650
|
+
- `__tests__/regression/` - Regression test files
|
|
651
|
+
- Test files follow the pattern: `*.spec.js`, `*.spec.mjs`, or `*.spec.ts`
|
|
652
|
+
|
|
653
|
+
### Browser Support
|
|
654
|
+
- **WebKit** (Safari) - Primary browser for CI/CD
|
|
655
|
+
- **Chromium** and **Firefox** - Available for local testing
|
|
656
|
+
|
|
657
|
+
## Test Server
|
|
658
|
+
|
|
659
|
+
The test server automatically starts when running E2E tests:
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# Manual test server start (if needed)
|
|
663
|
+
npm run start-test-server
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
- **URL**: `http://localhost:3000`
|
|
667
|
+
- **Mode**: Full editor mode with all features enabled
|
|
668
|
+
- **Environment**: `VITE_LAYERS=true`
|
|
669
|
+
|
|
670
|
+
## Test Configuration
|
|
671
|
+
|
|
672
|
+
### Playwright Configuration
|
|
673
|
+
- **Config file**: `playwright.config.js`
|
|
674
|
+
- **Test directory**: `./__tests__/e2e/`
|
|
675
|
+
- **Browser**: WebKit (optimized for CI)
|
|
676
|
+
- **Base URL**: `http://localhost:3000`
|
|
677
|
+
- **Timeout**: 90 seconds per test
|
|
678
|
+
- **Retries**: 2 retries in CI, 0 locally
|
|
679
|
+
|
|
680
|
+
### Vitest Configuration
|
|
681
|
+
- **Config file**: `vitest.config.mts`
|
|
682
|
+
- **Environment**: jsdom
|
|
683
|
+
- **Setup file**: `vitest.setup.mts`
|
|
684
|
+
- **Coverage**: V8 provider
|
|
685
|
+
|
|
686
|
+
## CI/CD Testing
|
|
687
|
+
|
|
688
|
+
Tests run automatically on:
|
|
689
|
+
- **Push** to `main` or `dev` branches
|
|
690
|
+
- **Pull requests** to `main` or `dev` branches
|
|
691
|
+
- **Manual trigger** via GitHub Actions
|
|
692
|
+
|
|
693
|
+
### GitHub Actions Workflow
|
|
694
|
+
- **File**: `.github/workflows/tests.yml`
|
|
695
|
+
- **Runner**: Ubuntu Latest
|
|
696
|
+
- **Node.js**: Version 20
|
|
697
|
+
- **Browser caching**: Playwright browsers cached for faster runs
|
|
698
|
+
- **Artifacts**: Test reports and traces uploaded on completion
|
|
699
|
+
|
|
700
|
+
## Test Examples
|
|
701
|
+
|
|
702
|
+
### Basic E2E Test Structure
|
|
703
|
+
|
|
704
|
+
```javascript
|
|
705
|
+
// __tests__/e2e/example.spec.mjs
|
|
706
|
+
import { test, expect } from '@playwright/test';
|
|
707
|
+
import { focusEditor } from '../utils/index.mjs';
|
|
708
|
+
|
|
709
|
+
test('Can type text in editor', async ({ page }) => {
|
|
710
|
+
await page.goto('/');
|
|
711
|
+
await focusEditor(page);
|
|
712
|
+
|
|
713
|
+
const editor = page.locator('[contenteditable="true"]').first();
|
|
714
|
+
await editor.type('Hello World');
|
|
715
|
+
|
|
716
|
+
await expect(editor).toContainText('Hello World');
|
|
717
|
+
});
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Unit Test Structure
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
// __tests__/unit/example.test.ts
|
|
724
|
+
import { describe, it, expect } from 'vitest';
|
|
725
|
+
import { render } from '@testing-library/react';
|
|
726
|
+
import { Editor } from '../src/Editor';
|
|
727
|
+
|
|
728
|
+
describe('Editor Component', () => {
|
|
729
|
+
it('renders without crashing', () => {
|
|
730
|
+
const { container } = render(<Editor />);
|
|
731
|
+
expect(container).toBeTruthy();
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## Debugging Tests
|
|
737
|
+
|
|
738
|
+
### Debug E2E Tests
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
# Run with Playwright Inspector
|
|
742
|
+
npm run test:e2e:debug
|
|
743
|
+
|
|
744
|
+
# Run specific test file
|
|
745
|
+
npx playwright test __tests__/e2e/TextEntry.spec.mjs --debug
|
|
746
|
+
|
|
747
|
+
# Run with headed browser
|
|
748
|
+
npm run test:e2e:headed
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### View Test Reports
|
|
752
|
+
|
|
753
|
+
```bash
|
|
754
|
+
# Open HTML report (after running tests)
|
|
755
|
+
npx playwright show-report
|
|
756
|
+
|
|
757
|
+
# View test traces (for failed tests)
|
|
758
|
+
npx playwright show-trace test-results/[test-name]/trace.zip
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
## Test Utilities
|
|
762
|
+
|
|
763
|
+
Common test utilities are available in `__tests__/utils/index.mjs`:
|
|
764
|
+
|
|
765
|
+
- `focusEditor(page)` - Focus the main editor
|
|
766
|
+
- `selectAll(page)` - Select all text in editor
|
|
767
|
+
- `moveLeft(page, count)` - Move cursor left
|
|
768
|
+
- `selectCharacters(page, count)` - Select specific number of characters
|
|
769
|
+
- `waitForSelector(page, selector)` - Wait for element to appear
|
|
770
|
+
|
|
771
|
+
## Performance
|
|
772
|
+
|
|
773
|
+
### Test Execution Times
|
|
774
|
+
- **Unit Tests**: ~10-30 seconds
|
|
775
|
+
- **E2E Tests (first run)**: ~3-4 minutes (includes browser installation)
|
|
776
|
+
- **E2E Tests (cached)**: ~1-2 minutes (uses cached browsers)
|
|
777
|
+
|
|
778
|
+
### Optimization Features
|
|
779
|
+
- **Browser Caching**: Playwright browsers cached in CI
|
|
780
|
+
- **Single Worker**: Prevents race conditions in CI
|
|
781
|
+
- **WebKit Only**: Faster than multi-browser matrix
|
|
782
|
+
- **Smart Retries**: Auto-retry flaky tests
|
|
783
|
+
|
|
784
|
+
## Troubleshooting
|
|
785
|
+
|
|
786
|
+
### Common Issues
|
|
787
|
+
|
|
788
|
+
1. **Port conflicts**: Ensure port 3000 is available
|
|
789
|
+
2. **Browser installation**: Run `npx playwright install` if needed
|
|
790
|
+
3. **Test timeouts**: Check if test server is running properly
|
|
791
|
+
4. **Certificate errors**: Tests use HTTP to avoid HTTPS certificate issues
|
|
792
|
+
|
|
793
|
+
### Reset Test Environment
|
|
794
|
+
|
|
795
|
+
```bash
|
|
796
|
+
# Clear Playwright cache
|
|
797
|
+
npx playwright install --force
|
|
798
|
+
|
|
799
|
+
# Reset node_modules
|
|
800
|
+
rm -rf node_modules package-lock.json
|
|
801
|
+
npm install
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
</details>
|
|
805
|
+
|
|
806
|
+
<details>
|
|
807
|
+
<summary>
|
|
808
|
+
⚙️ Properties
|
|
809
|
+
</summary>
|
|
810
|
+
|
|
811
|
+
```
|
|
812
|
+
onChange: (value: string | object) => undefined - A function that triggers every time the editor content changes and returns an HTML string or an object depending on the outputFormat property.
|
|
813
|
+
debounce?: number - Defines how often the onChange function is called, in milliseconds.
|
|
814
|
+
onBlur: (value: string | object) => undefined - A function that triggers when the editor loses focus and returns an HTML string or an object depending on the outputFormat property.
|
|
815
|
+
outputFormat?: 'html' | 'json' - The outputFormat property defines whether we want to output an HTML string or a JSON object. The default is JSON.
|
|
816
|
+
initialContent: string | object - The initial content for the editor.
|
|
817
|
+
maxHeight?: number - Sets the height of the editor. The default is 100%.
|
|
818
|
+
mode?: 'simple' | 'default' | 'full' | 'editor' - The editor mode. Depending on the chosen mode, functionality may be restricted or extended. The default is default.
|
|
819
|
+
fetchUploadMedia?: (file: File, success: (url: string, id: string, error?: (error?: Error) => void) => void) - Function to upload an image to your service.
|
|
820
|
+
fetchDeleteMedia?: (id: string, success: () => void, error?: (error?: Error) => void) - Helper function to delete an image.
|
|
821
|
+
maxFileSize?: number - The maximum image size in megabytes.
|
|
822
|
+
contentModalUploadImage?: React.FunctionComponent - A React component to replace content in DropZone.
|
|
823
|
+
maxImageSizeError?: () => void - A function that is called if the image exceeds the maxFileSize.
|
|
824
|
+
disable?: boolean - Toggles the editor into read-only mode.
|
|
825
|
+
ws?: { url: string, id: string, user: { color: string, name: string }, getActiveUsers: (users) => void } - WebSocket settings: URL, document ID, current user details, and function to return active users editing the document.
|
|
826
|
+
editorRef?: { current: EditorType | null } - Reference to the editor.
|
|
827
|
+
onTrack?: (event: string, params?: Record<string, unknown>) => void - Optional callback for analytics event tracking. Called when the user performs tracked actions (insert table, add image, open AI menu, etc.). If not provided, tracking is silently skipped. See the "Analytics (onTrack)" section for the full list of events.
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
</details>
|