From e156edcc6a9e63e87bf033a0f17514fabbdbfd9f Mon Sep 17 00:00:00 2001 From: Univer <68851825+DR-Univer@users.noreply.github.com> Date: Wed, 27 May 2026 17:35:37 +0800 Subject: [PATCH] feat(docs): polish docs table rendering and menus (#6926) --- .../default-doc-ci-chromium-linux.png | Bin 88552 -> 87967 bytes .../__tests__/empty-snapshot.spec.ts | 28 + .../docs/data-model/__tests__/utils.spec.ts | 6 + .../src/docs/data-model/empty-snapshot.ts | 11 +- .../text-x/__tests__/block-ranges.spec.ts | 82 +++ .../data-model/text-x/apply-utils/common.ts | 74 +++ .../text-x/apply-utils/delete-apply.ts | 4 + .../text-x/apply-utils/insert-apply.ts | 3 + .../text-x/apply-utils/update-apply.ts | 31 + .../data-model/text-x/build-utils/parse.ts | 6 +- .../docs/data-model/text-x/transform-utils.ts | 24 +- .../core/src/docs/data-model/text-x/utils.ts | 63 +- packages/core/src/docs/data-model/types.ts | 4 + packages/core/src/docs/data-model/utils.ts | 37 +- .../sheet-skeleton.integration.spec.ts | 3 + .../core/src/sheets/__tests__/util.spec.ts | 4 +- packages/core/src/sheets/sheet-skeleton.ts | 3 + packages/core/src/sheets/util.ts | 4 +- packages/core/src/sheets/worksheet.ts | 3 + packages/core/src/types/const/const.ts | 8 +- packages/core/src/types/const/page-size.ts | 14 + .../src/types/interfaces/i-document-data.ts | 9 + .../__tests__/drawing-commands.spec.ts | 144 ++++- .../commands/insert-doc-drawing.command.ts | 45 +- .../commands/update-doc-drawing.command.ts | 63 +- ...c-drawing-transformer-update.controller.ts | 57 +- ...rawing-transform-update.controller.spec.ts | 101 +++ ...doc-drawing-transform-update.controller.ts | 82 ++- .../doc-drawing-update.render-controller.ts | 39 +- packages/docs-drawing-ui/src/index.ts | 2 + .../src/menu/__tests__/schema.spec.ts | 38 ++ .../src/menu/drawing-popup-menu.controller.ts | 63 +- .../docs-drawing-ui/src/menu/image.menu.ts | 1 + packages/docs-drawing-ui/src/menu/schema.ts | 29 +- .../views/doc-image-panel/DocDrawingPanel.tsx | 2 +- ...hyper-link-event.render-controller.spec.ts | 53 ++ .../hyper-link-event.render-controller.ts | 2 +- .../src/menu/__tests__/schema.spec.ts | 30 + .../docs-hyper-link-ui/src/menu/schema.ts | 19 + .../doc-quick-insert-popup.service.ts | 2 +- .../__tests__/core-editing.command.spec.ts | 281 ++++++++- .../__tests__/doc-block-move.command.spec.ts | 117 ++++ .../__tests__/inline-format.command.spec.ts | 47 +- .../commands/__tests__/misc.command.spec.ts | 187 +++++- .../commands/doc-block-move.command.ts | 267 ++++++++ .../commands/commands/doc-delete.command.ts | 111 +++- .../commands/doc-horizontal-line.command.ts | 19 +- .../commands/doc-page-setup.command.ts | 102 ++- .../commands/doc-select-all.command.ts | 220 ++++++- .../commands/inline-format.command.ts | 5 +- .../src/commands/commands/list.command.ts | 35 +- .../commands/commands/set-heading.command.ts | 58 ++ .../table/doc-table-create.command.ts | 24 +- .../operations/open-page-setting.operation.ts | 14 +- .../float-toolbar/FloatToolbar.stories.tsx | 2 + .../components/float-toolbar/FloatToolbar.tsx | 7 +- .../__tests__/paragraph-menu.spec.ts | 215 +++++++ .../src/components/paragraph-menu/index.tsx | 509 +++++++++++++-- .../__tests__/doc-render-controller.spec.ts | 186 ++++++ .../src/controllers/doc-ui.controller.ts | 3 +- .../doc-clipboard.controller.ts | 4 +- .../doc-contextmenu.render-controller.ts | 35 +- .../doc.render-controller.ts | 42 +- .../zoom.render-controller.ts | 86 +-- packages/docs-ui/src/index.ts | 9 +- packages/docs-ui/src/locale/ar-SA.ts | 11 + packages/docs-ui/src/locale/ca-ES.ts | 11 + packages/docs-ui/src/locale/de-DE.ts | 11 + packages/docs-ui/src/locale/en-US.ts | 11 + packages/docs-ui/src/locale/es-ES.ts | 11 + packages/docs-ui/src/locale/fa-IR.ts | 13 +- packages/docs-ui/src/locale/fr-FR.ts | 11 + packages/docs-ui/src/locale/id-ID.ts | 11 + packages/docs-ui/src/locale/it-IT.ts | 11 + packages/docs-ui/src/locale/ja-JP.ts | 11 + packages/docs-ui/src/locale/ko-KR.ts | 11 + packages/docs-ui/src/locale/pl-PL.ts | 11 + packages/docs-ui/src/locale/pt-BR.ts | 11 + packages/docs-ui/src/locale/ru-RU.ts | 13 +- packages/docs-ui/src/locale/sk-SK.ts | 11 + packages/docs-ui/src/locale/vi-VN.ts | 29 +- packages/docs-ui/src/locale/zh-CN.ts | 13 +- packages/docs-ui/src/locale/zh-HK.ts | 11 + packages/docs-ui/src/locale/zh-TW.ts | 31 +- .../docs-ui/src/menu/__tests__/schema.spec.ts | 186 ++++++ packages/docs-ui/src/menu/menu.ts | 251 +++++++- packages/docs-ui/src/menu/paragraph-menu.ts | 157 ++++- packages/docs-ui/src/menu/schema.ts | 239 +++++-- packages/docs-ui/src/plugin.ts | 4 + .../doc-paragraph-menu.service.spec.ts | 383 ++++++++++++ .../doc-popup-manager.service.spec.ts | 73 +++ .../services/__tests__/doc-services.spec.ts | 31 +- .../__test__/html-and-udm-convert.spec.ts | 88 +++ .../services/clipboard/clipboard.service.ts | 43 +- .../clipboard/html-to-udm/converter.ts | 252 +++++++- .../html-to-udm/paste-plugins/plugin-word.ts | 2 +- .../services/clipboard/internal-fragment.ts | 82 +++ .../clipboard/udm-to-html/convertor.ts | 169 ++++- .../services/doc-content-insert.service.ts | 50 ++ .../src/services/doc-event-manager.service.ts | 193 +++++- .../services/doc-paragraph-menu.service.ts | 585 ++++++++++++++++- .../src/services/doc-popup-manager.service.ts | 15 +- .../src/services/docs-render.service.ts | 22 +- .../services/editor/editor-manager.service.ts | 13 +- .../docs-ui/src/services/editor/editor.ts | 1 + .../src/services/float-menu.service.ts | 13 + .../__tests__/convert-text-range.spec.ts | 188 +++++- .../doc-selection-render.service.spec.ts | 129 +++- .../__tests__/selection-utils.spec.ts | 579 ++++++++++++++++- .../services/selection/convert-rect-range.ts | 221 ++++++- .../services/selection/convert-text-range.ts | 79 ++- .../selection/doc-selection-render.service.ts | 59 +- .../src/services/selection/selection-utils.ts | 18 +- .../docs-ui/src/views/page-settings/index.tsx | 259 +++++--- .../src/views/paragraph-setting/Setting.tsx | 232 ++++--- .../src/views/paragraph-setting/hook/utils.ts | 10 +- .../rich-text-editor/hooks/use-editor.ts | 6 +- .../views/image-popup-menu/ImagePopupMenu.tsx | 310 ++++++++- .../__tests__/engine-scene-viewport.spec.ts | 93 +++ .../src/basics/__tests__/tools.spec.ts | 7 + .../src/basics/__tests__/zoom.spec.ts | 51 ++ packages/engine-render/src/basics/index.ts | 1 + packages/engine-render/src/basics/tools.ts | 37 +- packages/engine-render/src/basics/zoom.ts | 64 ++ .../docs/__tests__/document.spec.ts | 591 ++++++++++++++++++ .../src/components/docs/doc-background.ts | 59 +- .../src/components/docs/doc-component.ts | 4 + .../src/components/docs/document.ts | 246 ++++++-- .../docs/layout/block/__tests__/table.spec.ts | 291 ++++++++- .../paragraph/__tests__/linebreaking.spec.ts | 376 ++++++++++- .../layout/block/paragraph/linebreaking.ts | 122 +++- .../src/components/docs/layout/block/table.ts | 277 ++++++-- .../components/docs/layout/doc-skeleton.ts | 15 + .../docs/layout/model/__tests__/page.spec.ts | 133 +++- .../src/components/docs/layout/model/glyph.ts | 2 + .../src/components/docs/layout/model/page.ts | 55 +- .../src/components/docs/layout/tools.ts | 23 +- .../components/docs/table-render-viewport.ts | 33 + .../__tests__/document-view-model.spec.ts | 59 ++ .../docs/view-model/document-view-model.ts | 18 + .../src/components/sheets/util.ts | 4 +- packages/engine-render/src/index.ts | 2 + packages/engine-render/src/viewport.ts | 19 +- .../src/views/formula-editor/index.tsx | 38 +- .../__tests__/end-edit.controller.spec.ts | 6 +- .../scroll.render-controller.spec.ts | 57 +- .../scroll.render-controller.ts | 17 +- .../zoom.render-controller.ts | 13 +- .../__snapshots__/sheet-paste.spec.ts.snap | 16 + .../src/services/utils/doc-skeleton-util.ts | 3 + .../editor-container/EditorContainer.tsx | 1 + .../src/views/formula-bar/FormulaBar.tsx | 12 +- .../menu/desktop/DesignTinyMenuGroup.tsx | 12 +- .../components/menu/desktop/TinyMenuGroup.tsx | 72 ++- .../desktop/__tests__/TinyMenuGroup.spec.ts | 36 ++ .../services/__tests__/core-services.spec.ts | 76 ++- .../clipboard/clipboard-interface.service.ts | 19 +- .../src/services/menu/menu-manager.service.ts | 8 +- .../components/context-menu/ContextMenu.tsx | 4 +- .../context-menu/ContextMenuPanel.tsx | 149 ++++- .../ui/src/views/components/ribbon/Ribbon.tsx | 14 +- .../ribbon/TooltipButtonWrapper.tsx | 21 +- 162 files changed, 11227 insertions(+), 902 deletions(-) create mode 100644 packages/core/src/docs/data-model/__tests__/empty-snapshot.spec.ts create mode 100644 packages/core/src/docs/data-model/text-x/__tests__/block-ranges.spec.ts create mode 100644 packages/docs-drawing-ui/src/controllers/render-controllers/__tests__/doc-drawing-transform-update.controller.spec.ts create mode 100644 packages/docs-drawing-ui/src/menu/__tests__/schema.spec.ts create mode 100644 packages/docs-hyper-link-ui/src/controllers/render-controllers/__tests__/hyper-link-event.render-controller.spec.ts create mode 100644 packages/docs-hyper-link-ui/src/menu/__tests__/schema.spec.ts create mode 100644 packages/docs-ui/src/commands/commands/__tests__/doc-block-move.command.spec.ts create mode 100644 packages/docs-ui/src/commands/commands/doc-block-move.command.ts create mode 100644 packages/docs-ui/src/components/paragraph-menu/__tests__/paragraph-menu.spec.ts create mode 100644 packages/docs-ui/src/controllers/__tests__/doc-render-controller.spec.ts create mode 100644 packages/docs-ui/src/menu/__tests__/schema.spec.ts create mode 100644 packages/docs-ui/src/services/__tests__/doc-paragraph-menu.service.spec.ts create mode 100644 packages/docs-ui/src/services/__tests__/doc-popup-manager.service.spec.ts create mode 100644 packages/docs-ui/src/services/clipboard/internal-fragment.ts create mode 100644 packages/docs-ui/src/services/doc-content-insert.service.ts create mode 100644 packages/engine-render/src/basics/__tests__/zoom.spec.ts create mode 100644 packages/engine-render/src/basics/zoom.ts create mode 100644 packages/engine-render/src/components/docs/table-render-viewport.ts create mode 100644 packages/ui/src/components/menu/desktop/__tests__/TinyMenuGroup.spec.ts diff --git a/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png b/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png index 6026816bc527b327f38666ae2ff23aaf8fe5f948..63c0c694850055624ed3bbfd2c764cff4a21985e 100644 GIT binary patch literal 87967 zcmeFZXIPVMw>F4kFYpK=(iIQ{6qG9cQBkBTf^?$Nr3s<8SU^N6QluA^-g^xsD82U% z2|e^sQwT}E3*P=`qKNJ)0c3o?o>s)J{%b=&4$`{YEo};0mxv26) zNt=e|B>3@o@0sJ^ufMrekI~TlMWdp0U&kYHZR&Jw*B)2PR-tG3_h*(kx^T+%b4-G1 zH`;S_9t%e3T6*_Klv#iIRQ9<&v@>t5Q|*Q1$N{pOXo~bBcfakfi!sKK5Ad6ccy~Wc zBAS@L;|o(nhEp1Y7@M(#Eyfn7ZLLd3M>tf)iv&{+<&^!oB*cAmV1ywwFZP-K0AkgOKlKs6;k@{`|RhIh-=OHTPIaNl8=2t&?3gIrQcm3LAfax_q>75;!&d za?Fbb2>798Ypx}X60^)~6Z>v%CHcywOVStnS-}_5%iuw5I$=J4i89ukUj?72*qo~e z_X*NWyk}JK)d{B_1a4H#TL->6Jc5oLeGPttf${RYN8r{@UIH24)&j_R3ON zD&Dh}6sDQ%X}gbMY3P-KO32t%`ND*Y)bF1*Mo>;C-1;pOh<(0u*;S;>;x z_nVg9r$Tz|o^-z=Nog_HIpc0%n3fNzekXT8o?;ixwmsgVnlf5@bEfTj%l3%Y&f|{V zG4(H_@Xb0a1>phMraiHey4|!SoxsR|Yc4d|!lZ1;(zPrUxS@r5P};Y;<^7D4f2DVk zL|m`xQQO|z3oYCQZ*lk0F@5j`qAdM7j^FwkQ|_D&)rxhqgHT*X^winh=LwxQEyETn z?&JlVwlbET455ex^GW%a>d9sNRPn{l;m3mrIIm^xo8?J+A8I9432kdgi`Fp+`D?`e%9ytTuzY==0~s%O?*Puhm>#ugfLRPN+ZSdS=I53b7y<8_U!h_hAOc2W12?`^yt~LFR4CZ zl^iz0<+rt(suO#a}$*X z>DYp*9)NNQUt#9rg5CQa%VCSR@44r`FJenhwYc@7^2y&cD7f8Z+Jdax8iy=C`Hid2 zi7S`FM}N}X;^vO{C7R(#Hm_i_*s9`SW;)KRZ=^!a0zpb@~(f}~2$Y=)N+ zJBx{_bah7DYAq2e2v&=Vl8Y13_Eqk<^Q#JiP)HcxNrnm$NR@TduC(p>%UzJX&f6op zVrK(2j5oL~zMSNpth3D&kuhE$er&lusfZtLOJzOWoeM|1bB0}E|95?)tBLLS@BINf zJ_S_rTPVEoypYu`HmMS$PhBu}bj%I=fD0`cj=RiU(xQZxIk|H4;cyPm{>7e@3a@+@ zHA@bX9bv}S%RZz@)TViru)$5RSpFt56F?l9c*8RKw6<9hO zKOW3n-KgBO`NbB$5B|=N; zXk(IEJ)YIrZ7I81s_50Z83(F~<;lJ*v5+P@D60=ev_SPbV>@>f{z6*sZ+CTc&ejdB zSarN8Qz0>X>vU9Ye&>RE^d}wHy4e)V{s0^ zMfm8C!%bocDq|k$a4Gin`118-oWE|MrGsSkc1o=Bh3=FBRZ5xD4xPO8{HOw+w9vDO zTd+}&xY0=Mt=vhFZI6;L5R$NYRv1i_x$f89QYbG~XPwM(_N--J6&_(wWxWyV{{_YD zkV5La0BtghSq$pXuQ1%eFXoM+(%xy(0z0`HKL0!nz|d=*KO1whVX=17{r**7#B^uH znc6ePq_M5|1Pw~bGL%0wCc~yM59#-fJVM{?UHeOAnCZtQy&tIhnN;+$s}T!-ou$yz ze9Jna_RFo-vn5P7U*s<`F`YNx;hd@^V}3SJZVgmeq}w4XvtIB_Y;!R?NF4&mgCqYC zY{5`{%ooDNb%oL|I&Ij++ufM++U*wYg-n}B-F`J&!?^fa#mUKu8d{l#5{)r^u|E6Z zLTHCcMt}NH`#jaX69s^*B^jbq)9^7!&o5NSu=G_$wIf~9&of|s?>-3p`w%8K8ppE( zlM~#yZO5uIZv1iKP8pSxdbOFy$g-2xRZbq=c}VA4xXxAu-Rtw@3z2qyz2E>HO%wM% zvn%2xyO?JZ?KC<~m<*gV3H+PiE(Q0dlGAK%!*4F!OS9`2mNh1`7w)Xiu?Ayu5#xL4+6%h8CvB&74ELgVFFw-H#E*cFrxc%+vLX&n?1^O z3E~>?q>)qx*EutET@jUN&gGU^L>~EVl1waBPvFmCy5r>CA!+mMhjvKExdW)a%Y$R3 zmHHcYme@5s#7m0O&yQMmH7UNPY|orGK}w|;;gENLe88$LML*%@b{1BPd`l2djOGQv zbuQ_i)9g~88NYhQ(;og<@k=Kf8NCm~MYu54Qw7-yWrg1K-FLC+=baPnqeguD_7`)e z9SHw-wT>*RFZ=jrTx9>@&q_WnXUagp2S?+HL$^OXg3kVHBtDAePSSp}RYWZ^hVdGB zk^9NZQxz9;7}j1NYW8jc4T7WS8~|hJ}FZ5p0WeUQu%)kIx|~e-#`d^mhL! zgutU5`VT^(c%lA?k%WnN)7N4Xd0;V>N6!na36s|My9eA591Q1Oxc;3N`BzGD^U(0X z@kIS84d5$>4;_1iHHT;4y??YUhfe>%kjJ@V{3@~w{BENh`Pvr`>(p<&&rcpM+u`>g zjsp}wJpTQayZp|qyDzl`$0ZlVGvT?|IbLT)^)wJo*GlP5l>Evp;z{0b0+&a(>hm&zJ7IH#A~PF`nb>U;4z>9wlHOVbxrSydq>M4x=0-&m>6Nj~Bf%+gpiA zh$^z`NsfVV)_SxtaO&mgWZT9BmRNSZ;e9i)7_#r=9%8h5B~*+%!kJGidAqMA#l;#1 z8OqV^GO%3kPj5i^NudLnQQlbjYA8mM+p4$5brp_Zk~V8DHAu~*X4zIM%mg;vrS*OVwt>AwZ!Kc>b{F{nTF-H18G@!G*|HsHpz%O zzQ4tvqA*H)@3K5Fl=ZC6GK7`S(*a2i&0R4bD}LzRvnlndwvCx+H_y*ER$)KkHnkVx zywGlfq(~ePcY2U^XoOkgXMFqj+qMxiL}pVtkLj04Ki^4OdiHy7$rm+H9^~(UmdDP= z-P_7EfxDxJ^6R-SB7Q ze?DVGQuMV7VBd(bXCHxU6zb|-^vrY&IUoEv?K3cHW9^0h@p4-c{Y_FQBr~qGZ7_Ra z1A##)gyBugJht)-N|F1{yK$ZM;nZt%95QQdnaK(Z*1jY;H?%K4p86+s%3IuKvdV4U zaB;2F9gFh!KM}pQHUL=l`w!f4oET>)ar}h zOs*r*-(Yv67ceSmb=;v6och;fBy#6l{R~%n(%`aKyBWG)g$SG%(vQ{wz89??CmI(8 z9Wk*d#IQcviYu*g$69KKNd?jg=@(5~@hEo9V?QlzZ$9)TU9=v~Yx3u6S?F;8sggt5 zsH;Q`jr4B$><#TDzOVC=e^H^nJf8`fqc*Lh>a2*)79BAn*b6@Edr}B0C6IH9i@Y}i zIe$g)p4*y~<3(>$pF>#eO$$MtI+MZ&-juWTXW5$R{LCJbO_OpC7(Ggv1)~~BC7;Nq z##0-JyCPkOWD!RP8YUc+|&c!oJWVb~- z*{_OoMP?IYRUWR+U)VkvpV3&okgs5=-J|xwVG?esV4S%gogDdCvd4;dkFcDXGLWuD z-BuIP$?Nx?>@DHTFUPYiops@CQ`knei_F0@sUM~&D$aX zG}U@S)7!s4NQZ1*ws;qCo;i-UmKc2-+=xxmZfS2s;({3K{o(;ydP%#diohcHROes^ z#czgk@RPoJy;h}G9jMJ3Hc5v`{j8_gp{K~jb}aTmOdWb@8o5Rd&3~PozFOk6$(leeMZd&aulp!tU14EY)9CqAO}-@hZb+^3M5xJFspZz1hN$@y*yhs*2apM^ z=QMOmcf4l;VqO#&>lV*8@uh3T$t~pMzDdxr*vrw~TN&%IdDF6qpQ+bpw) z>GpQVM`gc#r{O#U$J*kv4;Q=R)GDM@zGXiR$k|Al@UP9?w$!@$to-pFtYzH)Vx$5W zX|;K3<34i~^4hCy*`7}KPxCi*>e}q}m^9lc(XE6J4y5o5YX!Zh=<=7=6BQ5L7dm2i z2I5kF5O2{G|Fae_Zz$_PC{f$T%ZWP^s&2b}2pIik-;!rl4lc+1?yLS=Vw9{CBTO&hFxW~6{jKn_J5LI=$ziTnafNpSWM% zc#hBLlJyd`cFT_Vrk%CzAvQmP=i>JvA;>rWW%Ae-Y&~{s;#WJ9>M)`<0+T)Ejn_&P z&9t8Ju~1{WYt_Cn1z*nTNl{!-`@43%Y+20I*W#Os?m38#M{_V_%6G*@yLN9lfp6@M zP9bWoVnW7cP26j+HH1w{e!Ddli==8+*o~>5TizjP1kW7&^|Z|fEelwqc=5@IxbgCGYOF3s3GPP6__ za)`ZQqjC2o!aO7GQl=O+n@Q+}+fr|~dtq|EVY$nxi1$6?UVSAN9Y_*|^t2~h;Q6QS zlRM~Fh`p7u2I1idzeT%E(6gkMAotGOi7_=C@+@+g+Sk}h@W`2MVitqy zyS0RINVRRYPsd2v9q8T2@Tqn-s&o*`Y?)~Y^u~GFk@xrRr!2^hpZo6D@ zlhbQ^D?=YS1NBTjLtMsdbk(?vRirQbp*ba8=B-lw_Lniq^4@^#Oi2?GMK38D#+baY z2b_Q}h#}f%G#?YFMjyb&U+^|bR(5Zt53YwcHDQanrIW79%(;VZ=u)*5(V5#YCHunM z-Z0q^E49#m%3*xTFPlFid`SuBtvY3z12T`G6jJJruH$%-UH1U%$>%tudv0Cq(u+@% z7Ke72@&^tgZu7LeAw9fG$u0wB9H^$fw&P0md z5H_rMLmpm-3wI_&1%teMICyy=<|gaG9i0+)+yHFZ#;rH``pI*bf9#Yz3ID48C0@=` zFi1!(`WD{;C|<2kREoSMP<%;ZO*e8Ys{F{G`chyHTrq;W+vzbMnG1}>k@9UZp-Roc zENE7lG}O=@h|F(gG(&`qDmv%F@ENT>YokFBzvSt})8A$F3WQBKMJXSUD!=^whjL>Y zr7_3&?bsEIEFz^Ct-Nr8CXfxI5mz~vPzJs4=$*y1A@}IKzpUu&6DrMnPF_@1^4`nu zPgk8nBj$Rg(9?J6SB9(G(;j4EjO^mXog-}S?J_{lS9|TOz`a5f$8ZsijPIeQ{bimF zzczGIO7~08SIF6NLHdg{`+oC`I7pj+VU_04T=gO?#6YS=jP7}C=0f}=7RPN1gAO<} zTjqxfOoSka^Obf)+vF6ESRQ3uk8)V>%9nC0yU0xKb>SEMkao>#WH)snm_@9ayvgwh zshg`8z$&(6e@PQR-v=+YpBP^3a)eRImwpO=XbLlddvA0nO794uZ%b@olD12BUUuq1 zVxy)9^%g}4zB7LUa83c+3+v?%to;%*8oC)7d=xiQ!YG{tIIm0@lG^pt@g!w}LLmtk z`Zc{5^Se-Szz_13XE?j-W<~|RRKo3KPdx>1Wrl{b+l;HtMKXXlhcozaa63 zH~NkH#(}K#u~9<7!-}c2yQk^dKR`22)YP?5@4f2Tkgr7h8Hv)f))u__c#JmrreWEW znLku^i>I00x~>ruIiKfXaV6A&j&GIiNhV2OUp6i5WlXRyIOfG~b}kVsb+a6z{V01C z3|%WxpDTJ*&r5)mL8l{(OBR);l4|5w*95kA&WQam+{PM2Qgb4zN80W``qbl76@RXU~G)BHgT)PD`orQbDw5TdAHzlVV!*m%ELGGoN`xHXf#94`vOP@ zt4g;YC4XX^*9ff;R!u!vh*8;DB+*TCUy!mJt^b_pcdmY0q(v(c5ZPFjcyYeC%m7}l z_3p0VHKFbTQK%%%6fC-A?8*>9(>gOD-)`cZST!JIqZ5?3xQ`v*k zXQ?0^smllP1XWTEa!?Iz4n@BM3Q2Z2*L!Hv^ZX||j^UpTPYFixZuD!OXz>WP02e^bcZq4PKlJ1<$L>i$|U(%9LvVx^bm)O5f!uNi# zVw2=jaIkNvxE~fLSN8JlL|}_9m%`4|B%j6JlAjmZ=%k!wk}~#itA)sH*t&$yi=^v1 zR6*UGC-|{0DT$B+U*L-0Zy7ibTBS(B>rS!|KAhhU<&-E<3SvOmaFR4|>|;C^>KE1R zx6yuh^5zVWru6ufJKD8fh*MhoZF{97nlhwk)}OKC`pIp1`h*r``;NZL~r6`v6B-898IWt zSjyFVf8zr}1i%i4@hFF_k5nAj$gQnu3?}=G!e#sVOu1LsO(9`uFV|IGUiM@;&+U5tUct~TCd)YJ+ghMWQXK@xsM^i7)-e@Fc ze|I2ku?NyyqMHEnR1+Vk_MYf77J|meReN-q-AMC%zG4<+kDV`wRXod48yQ8u!lUdU zFoEb+RF<|9oG7wE`~*SM79}~l1RoY2jmWl3Dj5u;%d(`>sv`nK3ii&O4Mp0G(y6x1XL^tMdi-gl9@FH4)?)5dTk zan9?{P1C&CpvS5j(oo02EG-RH*#vxgwyh$J&oQ;!vm;v1)YtxRD#CueY_SKx3uX*s zYW;}njEbs;7beuvxMsm_4Bw>bP+UAf+%lArU!^2eGSu?z~9t%grA#vTiHJbzXa4g#|mm0gwnGdaZrFgpEs! zmx57N3+ag~xjm(pU8=kG6B&DT_;Tt3CaC*%5`}hm1v^%e|8(*?n)rx1Wu6FawyREq zCH*Qm#yh?E=rlbSVmd?S{^K4!%na^Pf>fcO%9xKdh+MV4q!szV3RuD$3+3|m`U zDL&gOy+YUYrJ;*#koU$gn=Jg7UAx~@uki!?m_oCmyDB|E`3_+Yn``_Y$PM^u&5z%7gg zaej_IFpThW7Xxuw>qVuN9Y1;Q>Egog@CId0pH@9oB^9y1vf87N5Fx>!!gTpvle_uy zMjeS+*rd*tR4^W+r@Czrf7vexA!aF}dCw>9b@l9f{4s?oLRGz4kzSrY5oB)9=dTI1 zL`%MPvIloWnwG!Wv+s%@$%a})IIcYDAOJjb+n5aEuIAVVSyYBb z3M$hP&72JDPP92|gdr1}J3h8-WTU97HaB1#qg%}hkI@6EtM+=|)t#^38@qC$z_hL- zuQAudBnIYx_tALmkcApPu!TRFGWAP8{tBP+M?7<|tE-FA@YK}DVGb@0dn#~GCKm6>s;!n#4(Nt` zG{0l=-s*_sXi;%nNQhWd)7DJ|KPajcj=$f1(Z;3KJI|olvf<#75N+UdWWKXg_D%^e zK3TvUw~L?yO+#m&?m%NY5y64gqf6kxyD{njlStL7owrQ zec|fn3D=jFpXG0O3}*E}=UcakOW}i}8lt^yb+6`{Gn3huI6bE8-?Onwu^W0g4 zZ7sxH5HKuT`YOVEqmYhuw>_;b61l$(#XsP6g&0w5&MH#=8Vb;-4OvAmhIo2wO1!}3 zBqgsz`EGY$eZ(9oo#_)LdexZh`zEXtjc{*NOI-~eJ6xtfX)l*`(#;CaHzd+)V*QA_ z)OJKT$3C+bcY>4^Pl+n|XDt9w2nq&&fo^PVB zF4~t)80lm?2i zfDTs{OI`8GbI(fYFKzk#@iZQb=JHS9v4=&&`qNZqTB<=l&gC(EjIrSt`M#VjhfEa6(;H`R ze2IhwE16Bz?1i~_yA0={A4p6Mq^Uf-_%3vjS|r1P+FlSs!C+PE8&ot^c$H#Z39;(7 z8_Kz88?cpv+XH!kk{=DspY3GnYI@A+OAcDni{Hy_6$vlZu~x4Gj~6A@b;qH-K_6ni zm;Tr1K*@6|qDi!%Iro2RNc_7Ehnh3d|Lj8O{vWD||7wflU$5xD%QjcZ@{e{(I5cVF zpSQ+3m| zf3}jOTh_+lQcEfSY$!QvO8Xsn^eQs_KX}xCw@>xoZ}0r;yZ@UDI@67Vf=eieY%owL ztkmvwPat7P&|xxK=uVP_AbcLrC#KTe!u;?h;h02B<)Dk5hdn8!UoM-|zX=2n9%o_g z|FEm{e{tJ?Z1(@GeEIM1;lFv0{~urQsAqZD)`LDVHg5g_6a1HkU#s*2VEm`Z&yQN( zz-zz<2Z_?9=G`SQsvl>HJIYh@r2aqqf87%PrakX_AOG{;ul@hCe&0VeOaJlCUC+N1 zUfK`9N-y8A33+Hd+O%Z1uZd)xV^^frIAq;GSIYOm7JSYt(d$^*I(F2wJ+!g^yyN&^ zdxC#$asKDV*q!C;7ar_M*nGtUEGoMTI04T=e$R@P()P;c6|xePR4Omv)bRiNkA+9Cp+C*T}Y=UAZI<$M9)C#uld#hiCgwRhs_dT6>VFS~u*F9-`O^C(;CqbhDMGCzotswirjDT; zQb-X|MfD4!56T4>qni=6COa_8arIKaIYq6Lz76xAH!t?|*o>D;cId^4G{+~w0@=H| zj~&8kkSD;tY~%2+3|uEd+D~qiDDpn{o=D8iwBo0C3O|Mw*_29q4KlwIGr!}G zwn^qR=dM-PYq0IS!KC4jV1uowl=r7?rK9Z(G|JV~XFgcgLl;Ny9=!=e{PChEE6?sA zZH6XgHxJl@=-Z=3<>3db*)BF3(UE$`z2}c%&ok1+{eBn%WqQXQ;7i?@vU-uP!oIEK z2)%F0s6+8tEXUN+LSV8UZuy1+TU|WGH)dZumu6&`-$j4?7}z<%6XkbkSMyN-W3|J; z@|>0njNq@tB7cZlUfBKq;~vnh(u#Z5AEtEraS zq1nyzo_rZT$g{aNxms{n!*NsoN7eo^Gu2D*;i;YN=ZMp%>EkjXk~1d8l3TyO=mycN zURLvYySt=lwK*;->KH2GJd{ye=+3GHA!oBsoUE+F+fa^B5ZtjDyxh#Fu3Vr+nWl(=lGQC%O+TP8W8$zi$g~SQ}lI-K>gI z%+P+U5H&4(MP}7ANKaGq$(`G@p~BsXZ7x-$6h(pcPiN~jznRwDGMKw(hC&a=tZy|a zi7mom$-m?%)4oBV*AXj)7Bl*n=*|pYQzzGl<#~| zu`=ZpiOt$ui^~d#(=DOI0^FK+B;(@JUGy^C8y}x%ps}&zOIvwhLB|)nBg&u5%brn| zaHCW}ryf7ucowR)`Yn}d7m~9d#ZM#UU|oQe+TuD6xb0UL5k|)hq)Z)uZBtqhxsL;9PfYh9aZ}qPHSqK{-)!vf+y)=lIV0AD z{XHL;u+gP$-}WTAW3kH@phRKYuJLbLm(nIsuLL291k6lBhJHBAWPAUQ9G_ZrV@up@ z=SMtsXK)1l4}PO~xB{PbJRNPJewvZugJ#Us8QJgdJ)AouZqTNM#bG|g440oJZXGZG zL)4DGpujynTbE}`mq^YW(bh*)hG6fbBJ~mx`Pqd}4~SPw1*a1yBKWmvDjv=K@ZkDW zd)7py;A!`y=c-~Rd%|DL>;JCdOehQ9Fd5zVO;#DX*%?}T+lb?yKS5a}>Gi1z~ zZ<{CXPh6*W^jiyWq&ZNo4{D4Pi!*W*FVCUcP8UoqVy6nUMCKECRfkti^wPUyuRFD$ zMZw+Jj@JHg83sSOsCHdZ4vVW)CO8CN_V`lhWRf^GtQf|AS5ae&%$B9}42{(M&=tqC zW!PTpv%TB+-a<9btK65K?_f4}_ln@OEYAob42CKjbKlEjJXUG&r6bnk_+~fqVFp^= zDQM2_{JmpUuRWnH_I!GOez zef7y_)sgNrsKuJ%x5<&p*BsHyq!HL6VyQlXksTiE~Q%tkH{T7gLM5IoBFA9uX9a$yd+wg;D( zEi_Hf#H+5&@bFD|@`X{?j1$hXpivhDmaexqL&nRa55fucDq%6YTP6bIsZF=3a+>$n zG?E=|Xd<+>eo<>)emBC|1<@yx=#9+xVoasNTtu0faNm70d&ET)*V#CcO>3x~9_#K& z{fcdfh=?KY)^Dz#*q)<@Ia(KqADt68x=0=p^3=GC6?C!BY>}wvU#TZ744lSlt}YF zqHhtY{#}(X3Hh{>{jJsVaUuiPYA|(-_SoJ{t~uPIE@GQ^{C4oqa!0yEunm$hHS)q z@qd@R(Ur<~z{?Y?qvG~+McgWIDX-u!!_Nj>v)9_V@nk~C@uX@>_hsQ&LOKP-WbUsx z!msew!98)Wwo%B^)1%bobq*;e@JieucJRa2Rbr#o_`wK#3;9NShCX=(*W%YQhV zQ!!qKakDRXmCP@m14n|EvY5F{pJpNC=ZgQjpz}KaKsev1mqqpP%xV zt!5k0%>rtRjjx>#uI#K`gR1(%4^8&m5g(ZS1;B;Xjv};mKRYtUw}hW2M#l4Jwwf?y zgn(=B)cj*SlM|2}(-fnbtAqQlYafo=dsi?&KuGZe&II$ZP z71cz^0e4&Ul($dj-0&u|zf?07n!bSb+l_t;UA)t?hnGL&YG8xH;1Yc3Z&E|aDE|j^ zmCCk`6O_X9vZtMe_iDLI9>yc@c{aLcx*}-W@)+*CpYCoN_G6a)iUlq!Hbw! zx{K&=FnC{i$$5i;eYa?32fB}HNsd0~5J!i6ewh7qcyy<%A@PabSclhE$gi5rXUUU` zv(AXqMwD$6^COC(bQtTM=xj_kkZDkW0wXG>Ysdh{_dbY+p?9(F)Ycp7k70&5UR?(4 zK>#B1OVYaUesXbxB9aost4Y5l2r1y_VbQq66svG5f#{Q1ZN1T6p@=?fcbTc11fO_HjS78#q z>08on2V5e{Dkm{-Ni*)10&x=mx2Do!M z%eLuk_z>rbHO+O3gue?bqQHCehsb88${gwQQyaMpGG~rM!4LuvZwemX{+Hcyu1!ox#Q#O&ZQEo7tu^b}D>ua-bhy z&qBe-h&e^rcxPe{W=!C|%1^1HLsBgFtLYzQ;AACsZ-3x0s!YK0A3!}cvRE_%-jMw_A#|umlX;X0zD?vTxyk=Vo(g!VP6L2wnfDv@@|O2eJs)cy?~QZUpZbYR8wJNOt@zSA8h({rvfOQLDji zM+Y?QFt&0>F#Ip8+8kZ)-wY1@$4U5qCj~wXHDD6ye_!y>c>Z0b^nYGP_^+-Z@8jG0 zMb|*<-l8K~!b-`<$7g?cS)^+EI0*x~9iY76bwJpnkVy$kA)NC2&1{aHv3JdQd8lMO z8j}L^>&;B|-l+a~n*NG__Iv?+L-f!toD0-!m3a5AJq`uJ!>JnA?r#dd`3&{s5`?V^o1*`3f_{r`T!kvDJx*??rEC{s8q<6Zy}f{--l;;LzDx ztB;b)I8h7Ljmhf5>P1!(20TC;(j7GH1t3q(BuY6ZTeSlD9gMuEXLSaKTNzp%EjEiW zaf^fIIe)n-W_f|D<>P7ryqiuXMwo+|p}{tF|0bxqyMylk=K)@;_K|Amg>#A&COzZ3 zuNyymW9wqdCV`L~6w)vWKq3s}FmkC~n^5}dg=nLH_?x|<(2so2|8CZJz(>Ov!10s2sj6tJzD}0xXnODyo7DCb6Hi@J=ON1Ts0vQUF(QJH@aqFfraD2lv{0!}+a84Lm;KK5+*_ zFINDXlwY|R96amss@>E^z`^^BN$hkUu2P3V@zUXbwvW0{F51s_hHujyfL){vTNtRI2E zrU<#eFF!f^1?;EF)~>AmxH!fb!U<3Lb!V03wdR#T4u4d=Q^kH9jYWHu6X#C4sqda{ z``qO+wAX6>ZKH~B&M~(PUL#A&UpJkb(`v&l0xBdow>w1nm zU|Zyw4`_jPgV_w_P!_Qa)$fAxUb~yYDYEVx^-|S@*{@&^oaT@Py1gzkp4;%(6k_Su*Q*=SuR%;b z1w5e)G?$LbEQc*A?(BNAF!{#;0|Y3?`ywc&8<^K=TKlZ?Ve`b_r@2n}z5!lh))HDC zHc@Vi@pKz6vjIwI$?JKB<|hFqb*@Mq-Up6v z*&ekFzzx@v;`^QYN-^`d_QQm zNuC|?5jH3>2QWmC>bwV>dJ6cm=Vmr!`b^sX(@N3(`ADsTs@Zdx9?+!azaEZ)_Q;!0 z19j}Dc4t2z z>Tcgx{JG%mexjhDw;m?9-TLVw8>iROPhPHlHcG2i2UI#Y%VXo4QvL+=rzO2${aka% zd3F?l(Z?aIzPo+OatPeG?L>t=i7*#NC1O&3qB6bqRxXbok%Ppq03o{HxKFtfM13hz-f_k0OTfGmz`%h7TwZ=NTjA0NQbL`QgT_0Mr*h^w81@V(O8{C)VBWU1sFs zLgyPv;lBYy*{TNml`?62KM)Oos3T(iJd@!31>OtZU*=O_lxmEs;nB5253e0_>EIL) zdHEe2PB%S0vp$&p+}L;$n1>Gl%}g!q)Is*ygaI4;#uR7>Oo9%7`I{kO1Ys8Fb(wD) zx~vZ8M->uxLS;A3>Jm$_oJml#5$!`d67*r5%E>g9*w92KnH;Hwn53f zEVfq6W$EcDhIe51Ktqg*s`IvLj|vL9u;+|8b6vWV#F+Lmx{#59VeNYXc(_$gKQLSY zTwqw<9GD95;lqJOZs!fq8&lc4xuO7&DEU1AT2YFaU8ZJIy#H6Z6b73}LkHl4;GVwi z;ZpYFgCO9v_W@`*jIT)v{D)I-AgF`>+)RzU z;vtAala5M@K{o)*6n{3foqkjVqKw&Uvj7<1kfuF#LBHzl^)R*XfOn8itpZrH@w7zXZ3r@MLTd8u!xRNyFkv?9IoxbWN@r&gHMbrqm;#Yg zY0|ZS8(4V=mHj2ROKeyI2rXcHmN`YunuCG*ZTkm6jEyyW#X;gy`*$Jy%8)H_djZ#z zqnmp;BzQ?$`6wzLl0krrpFbzh0LtSDRK?k*r(U>x%+XF)9VsxOMW5D4kklC4$DCv~ z-W5Jqk?|B@Q!dndS*=kaM8@rQO>__(M`38hrn*q$G1VB}Y?He_ChDaUr&~f*82ZeSNI7?h?wHV ztTZ1y_#l106DY|+bX*i41>7BwwXWqdFm3VZ>lK!udLRk~S_sST<3eEW3wW`Ba)P`! z4rCBk5$`pEd&c4Jj)XosE`?5K{YVgfP^&r=jEjLLTRwGs$P ze`>#-fSb25FdjpA|Nd}No)!j38VCUwxfE(a!Q_=G97sPv);J3ousglECYU1QUb0ik zpa&`5hx5YLVCr>VghkR$AJFhz-CVc!yH;KC5`g0atj_?kZPVb&5&*(P#q-RyaG@Mm+4%RwU__`$C+Gn8><<&vC91&pMj-9NIY+;kME4u5wyVRHW<&}apP&a{?@w#l& z_F37P>wvwmC;ScO$~2`#BjsI|dI6EbfUJ@3dTQMU7V`!53f3sveWBPs@R5w`%B1Uv314I5Uu_TU zGXeO5L0d%_n6~r$R7zJ%*}DeotZTc00i4UY0*Sr-e9; zjwNV=>1N_ANP5p5`~aiP{koU$B|eH&g!%Q<^LD~oXzAsif?4T6POqDTXWAyIjiDTV zx1RGjclTZI<+g$YVoAs?L~y5BfN>)h!eWA>DIlbcoLYj=R1HNy<`1)M$rsi?0EcW) zddH^;a&&t&salZRu&RhN15O77iu8f3_Op#aWyRL0F-4mgMq7}LnNbhF0Qm^V=ydD1 z>mLW)(!%#iSme$-MSBG_V$w7a!xrfl+ar;6v=}*Ap#`s?vt@2=6zOl&$hPC1ZY$hu5y{|2=f&$I;gUw5t6m^9c(hQGy zmq!S5$JB=9bKTd+A9?T@_EtX1+rH&wnJW8alL=@-*5|KONR$Z69>c4Rawd%_xUT3R z@_g0N%7D*pRL#*@sVBcy2^jxz(^Zr{sVT{SOP(O?7Wmg7*hN(?7B-K?&N$kX1)+@}qEg@_pAcwO$1Mvj>EO~bigmaKm4=Y$sylUdx8dr>?2CEOdKIXg5*y-_ii)s+V zjbK=Z#qgMw!QZ8 z8JM>l$5a$y25emb-DmmhlH+qO z4SNOa!K`N+mVaHm*VA%$!H0c^|E;REN~Dk+ZIF7MH{MO_<0Dd-Aot0O53Vtq8F}0~ z&-rvKLuUNv!_yxawW@P7+LZbVpM-NOeE?amH^UigOkc85(aU4yw){loPPc3)w_yKe z=+fK|wLcuF*q5wHjcjuw;(gcX)^hH-Ed3R-4RrB4CwpYEa$oq%6l|_(Pgr$F7OY#B zO|R3gxB^mK!uYOmXQvj!20*@r3)U+(d&^o`XZ7PF{Ka)cccEc|*r~Jru0!(QDymFi zPn*MBKlQw(`|J4#fdz(tKhX4$K9t09eB@?JU#hafM4X!V7|S|vH237!r%J+Wo?pog zgts#@RUAhc0$5&7dKLE~M<)ouX(>A;W4Cw9*2+LXkNy@J)E8J`dMB^@ck@U_g0x{Y z^n&v>XjuF0XT1b5+T{*6tEb+)cZyVHE&7{7*iJY95%3%lDAd)Z{`&dMKfU_rO;%>e+8~Pu-y=7aL-539< zfOMy{bW3+B-5?+m(v6gqG}0|4DcvecqcqZ;N+T#O9n!)q?*H+dW9H#J8D9V@*Lm%8 zt#5obWSq~6J(fHqt>|KHT|^TiC#$tmRv5I8JdaKn19DC|VNuTVU*kK2r_Hu%!np+D z1O(73+iG8jaD;&f)ec^X3B~kRu%xPp{p$2f^M=ryF8cRVdk+TUS%ni2YpjMcen_l+ zlhkdJVo8E&5N4sGL|;dB=3RVKw*M2~H)V?89RUIm%j=DD22-T>qNRL&9Xz#oZ@ zJj;@NSpm25iaM!rW_{<0RO-j|*LG~}1*cwIo%=td&Iji}V0bJ0D9FWl0Y!yM^{0lGtP^S$p@qN*I?TEkZNA0- zRP)~HlqWT(tMyxcuhJD4Vc;-o@xLpAvcxgMg zBWm@C#6TUc=SFR?doi@!Kuapz3UMLcqUj5K+D?^bcg4@0!lImmmBbNUE?0sd2M>wU z#}@Wo=oW|)nMe%irDvIO`rLn&d?wk+_jP2yP-FAqMtx;QCC~jE;qgwNsLX7<&kcWA z)%6mwvDfIE(Zq@c16P&rm=l;JtyfZp8-8BrvH!kKn$GgF$FNTI6}$D2>60pD(^&l) z8$7n`tTm9#6O&GN=H#hEJ{9m}W*LklIBU-}5}og1q$j!eQ9bYQkf(M3*||%#S@8lS zDM~4|6Yag3&UxO>v#lS;whiI*rq>V-;*v9ObTvi^j5C*Cq4>B*{Nq|)W?1}I^hJ&} zA+rNjD0l1;cNF^<-*cA?c96RK(u&7f5V<}fjS$(CMrlON*VrccJV*~mC_h*?AnQ@V zf0 zwaO4kVGGQ_sgLd}#&%tf6+AT59d%7BSJ z#Otdv-<^@%k+l@VG80`t7b5#6#@L0|mg!Lvd>u+79Tsx&kniDM!`5f6O~Ki}fg=qd z8nd`aEL)?fWr-v=U?rWx!OU|5=7Ifll7p&RRjCK0i47V{3V0u2*%cwRTG4R~dw0`+vGBd4eA|lt;$&lcp%Kj2vgWIOm!5`4Ju3%!0 zxOlA2ZNfkS_K~5~v7ZJt-7yavV01bM^DNb?e`aIuwU`iPA*}FOVRu`CcphpFkF+{2K~1@4H+V513h))~oCi z?DrfKu39QOem#3FAybFSq)&dsebi}qB0Ffk<~({DCpN-ldOgwCb)A&HhGeqF`tAg5 zEfUYrPyhEhCj9Y99B5aGek#&AM*DEm#!2C}P9jyqT00?x{iCA7lyUH`XCCeYmam~A z{Vxlt(S$du`L=D!aE0fmLW_w_+Yjf3gOSuEXa+Uz<}Ntl)KsIkJysUaZK!fxSG%sQ zMYTiV8v;q!-3QMkI+OH63~>abM3Eu>y|_M_;ju`tLk=@zGAIZsm~FVSWNh@i2{U2% zWY%{9jS7K!woH%rH*S&x=Wp496ix;1@H^7VZ;>BHrv=>?j%9FUgB*vt;P~=#oaFEy zi#S8RYa+URgXw8$-Tahx^4TPF=^^K%z34l_Hj})I2zx3W&no;#pM1iB^()_0l_68n zjD<^Zdp}P){MJsuq*<$MNw7*MuX&z@)cy{WTp(sm^xl`;9J6wwy-YGYwzge78`23} z>!;R~CWbC=C?6C^{5zgd+f3u=GL%wTbFV=CRaR-(=wbO2SX9krn7;o?vc038<9Yhu z{g%d(_k8f8BSZa;38HD1X-=CQOdgDr#3Ojy_6rtP`564aK+eOvcRtCESI57OYt#RF zm^PQf+KEtRN*%O{G-Sp|J*BA_S2E`Z2wtVj9l<}1nGO&1asx^HZ(zQdah{i+zDjS> z@%I(=8Dnv+(pOlD+-H?%f06h!fmXJ37ukq?mi7hrQTonfbxljh#*bUpav^g*HLaW> ziY-oB@kb(`6S(nw*q6c-^wcJ;h?1S&uB=HtJ zobTVHH|tefSsqSnr?V1!PZX<`T+HBQM#sxu`q0y^Q^5#Utq+sTb{DJd&lxLb4&kVL zdfJRvZX+n!e^)Bxt&mRwd^|rtkuk_=@%Zy4Pfe=8LL2M}jXk-!ELyHus}B)s&iMzY zSvmhE1d=_eFPVfpU5AMdBG=RgQaE;sUZYB`SqK!6k_bKJvnlm^67owtOO8sF?~vX# zvLy4nOi#cPhXoorUq#SQfnd)^uJK5Yl5fedp9+zq_39UD%-LBZM3jp$lE1SVz1Uu8 z6x0h<|1uS9X~!bOy9y|`6>0GKOcVEScQ5ldon}|-eE7}VvK47Rh9jL#*$wJqHrmTR z8n;UDE`C-(dRDwBd=%?Ku%mBPwIi5k^>-;42mkeAz>VMBy(LrYP{qXFDHN+N0(G&2 zUb?t=R+c|iMAP$9{f0F*37@2R*S%0O+L5%a-Wa~=iKAY>TTV-Z@|jD!Wa0EBjTyJo z`!rbLvwBw2VWUWVx_UUk`DKpeMefnd?tr})@T@GiMzKGcIiIMfdw{FGo?t^xfi>zGg`lbUjiuGaZ>X^}J z;ZbAgp zTbxL6R)jChmO*RR+z~fq+j;FiUc#DaGM3dnmUj9+AYAm06KY1R)^sTN3E)G!x*j~w zI?P_4e2xu516ujZG(DMKH|%G{GfMMCwSPM6^!@qxB3=2o5f?*Bk6^YwBIFk#?_DU_ zu1dsR_WI+Sd*8zl37UeZ?mcNS5k-1uy%2>s-fmFoNsk z6@$x!hbvP9X^p8BUh$32)!3TfxT$fnKl@CvfUEY2e}BIhR;k3_8&jw%t?a6HkLzBA zrA_6_Vyp#=Pb9m)doDv}x>g~hOWYLf_XA!<8%>Ds!Y*@-S=-7y%Z@r(U1~2od+AL= zOI`(+pMWPl&eZ;!oXh03IAXfxX$iG*&ztA7BWLC`yn(Xzt`8RdUJ>luQ2^$Brk&GJ!~BVfo|NJKKAn!%@4xhJHa*eB zE;`Vg{K*mGa^ifi=LVWY1FCc;R43py0DjU~E5RR~wz2D5DdRS~{ggK5lTmXigU&-+Yc$zD2~bp&ZD!j0|aS8yb14&lc2_un>6Z=hW=jU^U{fVn=x+Ec~JN-G6*@ z{qKFe#(h(CY03|N0IWdx;Ofh=(+Z_=T%JCc8uv_BiQxQhGXvhikX(0LGyEMf9xO^@uXd;`Ty+LSQQ?%{T$#@2M%9t+xzcb=7}+2xpR;;xh?g?JkrVh(vXmc5s*I_v#|iAJPNOZemB zk>9(F#Afz7UWAW2;B2BrogmpSx4!%6+M(GlY3y-*bP@Q zx9&92D$QjTYD(YdibIR{Ei>94bc&1H8l-xn5 z9^YLAIsFBvVq{F`ld)`gIgA^hjb zCcs^y95J)e@+Uc*((#5N{zzXy(I<|$y64s7D`sZ+9g=4HHciXq85`U?;-U3Lvky|9IOua|AJKJXAc ziY@^(om*ehc-G~{er##HMw|KfoXkQr!zsTB&GKbUfiL4u}Qm)3M=hNjBzGVKy3S86yV!Imz^2ge!^< zOw}^6d}qjAnk82e^lCdxr7 zDck$cdl2^to|As-MvFSPH*S*L8ieK8WKnX1%uM}T?d?kYXhQh+ad{;c&zsBmL*Cao zmU`8%5jdTQcP?dGtWA=gpDDd8q84R1edJU5T7yK>Oqt1`$}fRNu~0j1=4ln8wH2WG zW_By}f_xk4GX*kNb{1atFOvNJysnWtB5@fbvX3rVr=ufqN`%W{h4=Inb)*rS!5Lmj zze0r7FH3U#8$e*5wlDZ6j-TKPk$wfc!#<{*QL7@UE<~StWgnb0&@bl*EGcVpP?2|| zv%Eit5m8g`^&w0D17FuXE`#x@fX(RZllX}l^~TH|!UQSx-10C1r=Ma76|Rpd^*x7I zgUa^9Ga;f8J1K1X&YKa7rDY1J1~C zEK91S%2{`WVTpg~6K9wmDif~BLsLH4SFrma`%EOQAx@h}4 z$?jeA6P=|rH)0K~{HgAr?NP$GEeQ79%6dZzuWJ_44Ntm<%ho%aAlbOya_7M3+yAtH z%U$z(&cBgXZLB+JwlwY5jMqOgQ{c&;%aySX`VtJjzKe)D7x-nRvbVX+elXzA7Y}6% zZ|0RMhuP@e+t7g|5IsIE&Tjpv-xKN*qBpYM;=ShUx(*zt!1#WKL$dyq{zPMMBxrQO!OU>~5}+$JZR*SIWqP)F{1RFLN# z1)(Gum(Phm>!|oBKk6Ej?lj2fO}kyY(faSiz+bRVBz7904x%M2dgDRZZZMW2O*!FiKHDXD z-Ny>wxBNu&0q+%+b=E9jB!?<+;EwCpvp?!q=2I%57ZItc#so3l;B)l)>|*@j!U}ZI z(v&at*_VqzRgI_f)VPEd-<%R#9OD{-wBsTOg*7a zQhlG9VmAJ{L~6Pi>2Xo}k&7A8&XXYYgm*CxDEMs~btc!i-I?6hgK3Z34=lLy*?*;o z#H{a2Ds8PWue-gFcv~T3J9fq?vQVU)dTVtOx}%s#ym`v7v10*ImFyaWG)8LhRETP= z>AkyO8W%<>GN=*w&6I8b`nSvd{!L=S#xc!t;(bt%`_XSej?}aexSnv_B!m=I4OR-a zj=tT8LB0l@?Wfk_3KhG;o85KYmyHDcx~I&nvr-IyJIY(Hy}>*@8DE6=JVQ-&r7`$n z+r+5a>~0co<43%u_}_W&!7d4?7jsdA`)Y6h&QJCKyS8tY$pr&8z0Jv|MVFgd7Cy~W zXCtYpM@xoZ7no1)d5G&i-ZLfsab_Q$3BP~@N#+mmGUcR`JOtd1VyR*LE~-W05psTc&|j;H5N>UEU|3i8m(szc(5)0j=JWDT_c1(N-UnIId z$RZ&u-i}ROu&J2kp(K(-udqPo!NGXj^_D@I_&2qdC7Z2Ep6d?ty1~AmgV)3_35D#o z4$xk#|hMiNdsB5-TW z1gi2Xk>S;mzIhJBt~A5pgLFde87k>d+S>65PM_>6xiurJvNU$F8B3JJ^Yed5QGFi=AZ+|0ros zVYumh?OmjYn`EWB$wI-a7;)qmbT`O=j{0Jmsge2sy(!pjWw}~HBdRdx&3m)cu8Z^I z3H79T<{Tbny(cr`v#Y%g{T9`t`5XPaL4$*tG#u3$+d3+8W?sM`eOOJs7*TG2C5roV z$m*F7C5xcW9i`tg4^D;8hb6*KU$)#6W%eLDCaZXmxlrp$#C&4w_I`S6=g5WlqiZVv z@?HX8UZ`Hcd^KOS1N z+aDi@IuPT0Rt!cT6#OMfi$(kg&Np~@$cx>K@@v`Z=Gq9xWg3`2`Y{wFUZPUkshReR+lP z7Pkv|W@@~U?JRAFM6huMcr%5&?ehY+-lviUG$t`}Vz7O;9ES%rrj?|d9!*P%`tcI? z%5KWTfh>>u628tRmqnix;hD*ivnelxJH`Q!1YLRPBLxiYYBXNHgN3|}D7Me2{H7N8 z9})sP^d9j|?A*QS4rttj6u75jv66E>Q-tm?qpjo{J^gYcai5kA%2)G`=ZkWp)8+Wf zA+rZc4lPmqotd)IxP)hPUIF7g2IBnU-lQ8zX7_THRH&Au6?=VOqB~~K9UVrNah!0g zbK|IX(o*LLkH`~9+LIu=ngCVA7lSyeISKbDo>j1Ng6iP_38}D4p&_}Rd-{_?QMp^U zNjzuc`EkULFfYr+ML0#o$rh-$V`oO^pCx}`QkYW~X_U7jV7$cJM{o+`Gw;R}p|cOb z;slyb^uoEMZwq?u^;g-i&DA)Oe@;Ua`Pqns7qKlEeLSk!mAp5mg{KeB?byk5y4on& z)f3G1YQ??@eK;R+DV!Y?IfuMkwR6^3S_LHbct`d6PwYtGeT)=kaUI>O_fVCG^DCzd zu|Dt}0HK)V^^#E7XS6i`=}P1xAxbZYV&mw*Oq{?)Kox?PX@QA*7^e+=p2ojKe%m{^EP- zw`FySa|<|EK;ErY7r)%Dh9ovnk&<~2aU`$W=28D{Ld(YisJEroH{hN?lmr>O9Dt#a zsZr$taPaB=ua3ZubGzH##@`R1n>Jtk`3TN=BbbQ6Z}uUk%MU}o-$iG$_!ef)A)GRz zbX?|o+$j$Y`|XlK4i2Cn^cJuq@VnU^v3yyfDZGr-rFlF0p3Cq+k7!vhDZLr`2Utgm zkQD%aW?%Jvj&uFP!6G;umcfjf^!a@aYAK-;^3idsU zdjuJ}BWnnIK}>c!B?qQ-`1`V;0#o7}4)KLsN2CQhG%B6M?7D4AJa8$8&TyM`Qib1G zTt{IF^OKhnNP!O;E1V3$qtL%4fNRf_ zGs$8%YVri-D2oQ>x{Ke@oB+wE>bng8))M_ttZt*mHu%{C9_wM>K8Dn<;T67m8QbPF zn+IJm@Jo*<+f98Qa>4lx}{bH8@0WAW87BXB40S5VykMABK#eE_Hh=j&k_UaD)OY z+^>`@8;8=(JQWsI((gd zh{L!U%eQ$Ghq&=}|Fv}Mv?8P>NKYlE*gyu+)_!c(=b(#Z^MQiZ9jmbw2pfaBy&DM6 zv(Hi>N~UwWy)6hpOE0JojxgyH$08Qtwu_`GC{TzxLuU;({HnP0k(IvN=s&FbkPv94 zyuWjH!cgX!t!};3pT=E)=Dff3xP|a;P&BDbKqN;p5* zLaX;X+yCX(pH`6;4lShuMb-wBjf6@Z2t%nGH+pcqEBu$le*)Hh!Y0 z;~rINrR@actKCCpkTz#J#8=?!Qgqa{_DF;gh}uk6%TS!#@Qa}T7vweEj($PNCTWm) zv~HPdXw@h+O?wt(uUF_zP}#OK@Xg>L49TPg8!i1a?qTxf*E`uRr(UYrV(I6j|3#@j zqR;Roy7gBr`xw6LBFd9na-A6$r?G>K(KcFAxc-=J;W#}5p~Xh~f!#a4`d{RATs(AkX+oa*-#wqq z=HAr7nEfbKLeyBm1!1!(6#vIk?t^2>10DdEGq?@C-1f5F9~_R=J)lc)ZMiyBX61ry zb{Zg(vIafkWU5AFNP-J)1htH_{d{5D&KYZO-$bhKrhEel0m6CDL<17NGE0}cZHCQf z-vDAY{!;H}QVit$xUMxH0oh#j_$-6(t?iG4WZTlepxf1v93<-S484cZnBi*RefVg8 zzLnJ8o%76;%ukr2pCS4b#lwj8T>s~wxb*{7k=eH^=*vz)1V3$dE7(1yeU`XYA4xqM zeG_a`vz}Y{KP>>P{h0aPbZzzYggCmuqt{D>#|n~+-PKAO)xXITp1hwK-)Eo)%O+I> zEg?CD`bo;llG?)98>g#g-r;4_jxcU4kF2~(Dxpwznels((R=M?MUI0^B74hTXwl>T zi@lZEHxQA9BoXiRP;9_X4|mAZpdRhsP5i@SRLBx14M#wc;48}b zHa4dAAi-sF-~F-%$c(nndls3;aGCG(8g8)L7>kALG;Q>y-U;OPIEX z%NqvO+WcY=H&Z{nLyjjG}m#te?X_FWWND2cPm^whZS5<@Vc^;jFn$xxDINTCsbk)_!&GSZ!l# zgA7qj=gv;iwj0F^i-=qC0z1-2?2}D(?tg?PV)i0}j@rp|_L$n%iGnfLzhH!R{hqo=Y>lq8k-lw28A&FfPQd<4_ix zZVix(L(A{1cy$%i+PrmaI>A^_*;~#-`Lph&w0*(jiP9%@>UUYuY)Bf&RlcX&YGohp zSJ3AgJvNLMpkUAu4e=18mTz%+_R%JFE|?>o7fFCDn(yyM#U32hwRm<*Z&7y}_aw+f zYJ6-CksUUYY&TVNyQTO?lBh-sOW%+ex=9xmK|NwO5Dq+=`x@2mIC-@_r?ezvqlH8Y z1T(@Rjo{eSY%kaf%pM>*ybW91QhBYXRhTKO9Ya_><)Z(_Ry;n)xwu~|%0vEXh1~IL;K`Sg`oR+3 z#moyXmZ|P~jm1>{l)ovog`#)n+=Y%;&L%DDT#omnY$qW^XtjO~1k^Nmebg@OD)xPe89>XAq{&{7h5FMY>&v~!GR5Kl3Nz$Bi z#YEE|X63yp!x^^;tl2SLT9xcet&KriBmCFr=oaMTUoU6i{Ws^xu48f{titgTee1(N z8yC?%W%FR71rHsR^qY^ca+O0nEBP16IvqJM0?t3B4)ca4=d z>T`OV`J=Ll2J!Km_OR~EbfK5|lxEXdd*-%Vdzprf;O**vjN$9*|2Pw^ z;2E0@Q@LzUrR#7+L?QaDnXD#ChGdJ05eGfSCR)cud^QRl zUabIgC}}&-ZAsl62mPC?3Av4CC`46Dv`fCbElBm zVQ8Q~3NZ+xu>1_;uMU@v8Zp+BK)I&sn6w+W#;+ak4oH|Ht?gOrk_UC9eP)rDG?;!) zeETM+O6G=Bh|_yCytM2YAgVlrzOj=ZqA(J)gPwR9-81>k0zzez2h7BkWkVmGVw|K1v#UBTcypf-8nGAMZ8Y zsm)`#M1S>jDU2*TYUnyAHiNn0lk!_2V`<;amjn=8ZF7J5bx>5gqA{WD z|7#aG$iIzsoGzZjs3u*I9>$4f(;vo;O*Z}Iusi@^;t$ee-1*QmD>09sarsX%HXErH#e&U4hdtHuWG)fEk`o~Sh(exJ%&p>R2 zuH@QPpUMe^kJIyeMlF<$5r7pKK5K+Km2$6%t=KSH{l=mnYcP3`1rV<`rfHe3)>Ni6 z<6a8>Mp35M9&-WVl|JR;=LWmg{^a8>+}I7b-0C=N{tNF9I-ito9(bh{T3n%w-YyS} zw3F68l@E(Ee@y#7_U7Imt5)o$V&BI{&O6>+c;__L;QrzyWW`m7oC?N0)}OmQ1L2%*`%i z=x*Il|J10FCpCRu+uHs%)wB4O2h{RP zvAuQMwsna;HV;vmJ@9R8PLAD1GBbQKzJk zt2c;lLQtM$zLO(EiNN(M#PL1!AxBlEoe-+u+EuvjGCSCyr^%3wJRyHOFaA`hPD#go zM6Yvgh2>(f1M>3;pR)Dr%9&*1m$^ev6Mh9jSJkMzfhqj+0Grz93tn}F& zD+niO&R=Mm4zG|E4c!Ioi>%LGQysJu+he54qW!;!^0^XDTWeKN$O%|UIjK8*LoQ^fl(9O@p~W^6M;B(&YZ}kl_tFN3C&xdibET&mAKd8BN(47zblLTl~+6 znLa)GV0SDr+En1D;*eOD_V-Of@J3mJB4sr>psod3`R1_YKsIGmd5Ox zVfRmZqbrKmNaP=NpDh@&=4rh8dmNK_7x)z%yuuq=+GBc5k+>_;=<=yX-YhSpFU0y% zy4JtHxjnNDRMtfCO)@cl@Q@jN^jXdi>58&0z&Ib$0ihQaZY(O8NtavM`g7WWBe~C6 z&3APdoBeAp32N^6CHS`$t|pQlW<{SQEqZYg&=e!HkSW!t3|~!|Iq>k>IE>nH*^^C9kWWr6;xWs}K<^37bh4RTzeNe<-B3ovf#Q zkR@?NAtZTsn+vTi+elV~N0;qPH7|eowZRZHbx&3>+`P_>ta9lZ3+*sYs}C-`ldA3 z;;04`r_@y)h9po-w#V^3|-h+S)Olm{v=1>Cg9COg{PU z8n7mgAr)!_^m8p|KchFA6tPAf(m`j>PFK7ahl%^=VGr>FL(@n)P8(n2duHUHQ{g&1 zPi1C%v}I=AZ2yhg^fX&2rdNCR*2jVe`L8bztKwk&Z;|v#zjL-_NsgrPqyy?EB^4J0 zuA`pMznX3;OglWofrJrxXK**D>syXnprPv!KRadJmex<>so?mw47m+US}(ymQ6EDa zT7tZ+PhsL{+_s?I`^sm>dR&@K9$b$hFTvn2`#0y2R2qGFwK=_DqTRHvm~i}iZ|HJv zj+&CqSHGRPe_X2X>7nPVbt2r?8e~HM=dwP0G*GHJYgA2}E~HxKRw(oTF%vjJ*N2^X zW_J}X@{T#dMDG-I&Y^zkAotzf0i!oVQL;A>rLO9D^%=!+jK%8GXw$S%Iz-=w#|a^o z;>uIF`Lp)Gxv;Z;ljoA~YWPnIkr&hTGxOjG>XK*AZ2kjq(eS32+m_JXG-|!N78s2u zR{P`dB=453hvQINQ4ct`h_>?Ff*Kry=@X;=+Yvj($e8(FM-j4&504o2Mx_{!UK(sM z5+nal3-~&=U*y|AuoJ5Co|O+$P2xe9qmMK0U5(FO;>^&%?wS8$ezMGFlptjLuf6~I zDvAD`?C$(^+zlJNDaFUy4K_}29Sc_7n4({B*$V>`q&>JKL`X2NXh-VK#$v1F-83`o z&8f|H!5ayr*{en^NvX%2`!PS9?Rs2xO^V_NN& zqUN=VY4rIBk*GY04E}jP{c?hkvof~NU-N-z@lMdmO4@T|M8a~bd#u=aIj4D7T9*Cy zLxXvcSZ}BOaT4i?oNPwxS9r;VhJq1UX*=7wZ~Eu!j$(a>>7RVTYzj~Zu~)IHP$(rs zl_f+D0msnu#AwE83X>ytz_~S=VHTfV*3F zqWK3oLMiQE=Y5`63omF!8=L^TxmYDfC zDc*Hr76AR%i7w5pP0w#0_pxogMf-)K;tyA)fJ6*YXC7-;d-K0c9@%j%y3CC|Sj zDX?20XSiK`o)F6Be?i=ra-o$;XSo9Dz)6~}&#;?SGH+^` zVB?VSZGd(^_z>MDJ1)ZG3Vb$b2uBU7D2wb1J!0IfoiZ{fQ<)E-Xgq?}*{b$F6aJ$>y)gaz%>oObRP`9>_Tj{w>rW2YxGW5qNt?Yuz1mqWzK>%bA>Jajs6y`OZR`zNH*LMq)${s zk7{1$npMX>ghA$7BY{SBLiXV;!u+og8}5K>Uv$C{izwxqXA#zXp)84`xaBasfL?$g z`2sY0IK){TI8PtVqVM`h3Vq^c45Q`1`!4Rn!`9~LOF4I+;nl&yj`<&33mj_-t2d2H zOZDp(OEjZnw0{$5?E|^@5t?Zt^@>>1aI{!pZhD5zpmJhgnw ze@kW~dCJ3j#WxICD`$WJ@FtuLKdsrc-JO_teB#l{_2Iy_;7C;uM|_=uEMO1O5E*F=<A(13RbR_QWTEBA##b9=9#?>OLT#VsMpH?db+~GO>5J#Hk1C0~ZJ?|BxTk4y z@2%7QW4RW4RQt2z2VXfamQ21pqE$_}qq3TwMp+~90>=|(WvqAfvWB$W!oG0d(TWFp z9lPU*^5>QdI|3}LB~WivzTYYn06oAUWNurN%Rw-x`KM?Wu0C^K^OH(lMlkCok~Dn9Vxj=D>M%M{SKmkzLcVl+LG* zS4~ioJMH-QbCbbfsnX^uHX`k{8&F3kfI$Qp1Q|_6GrS2M^I;STqCd#*ZoBKB_A^!g zoPgOeqovL=e2YArskZ(SiuZ0D^aJV|53(O;H8HkO+95Gfn+^KpS079?UXM+16ux%} zEVt1g_=@=zP5|`UY`wXj^UJZ&iPShfc8UJOSkxUbUFT|Q7YAZ*g1>m`ev#fbg*`~(fsQP{1%R{I^` z5vJ&EP1{zOSEf1^XABud)P|Bh$YcBN{<_`XxMU8RrVSoDN`f=8{wF$rz*4~dh_%JU z&@T^DIk;STJ^l3$znFuw)fE5AM9T~>NlI~G8%xdW(|fM1xX5k&S4galX1vdKnzuGg zXcw{$z0nC9(EgmvqqLAoqS$A%r?xFmCk6KvT;7t8OdXM$Wt%9OzONR*jvx6i@1z0S zLu-)QN0Ie}*Xu{%^!POAA zaL<|O98)R0SY?ABd3xr12QQx2Vk2-RYl@t0O_fVR@?h?%`e8jNlz@y-iOE~e{J8}q z=j;Y1_q9*DCV8Q$1*f3WQBVd));u5CcEb2c7R#B2L+&qSJt3QWZC96M-{Iymz5?5z zl3uTuz7LZuZ~}|dr9N_R(2VXL*joD_BV!+IOqEmDW`NT?{n6px??$1#u535e7cP*< ze0x2gY^@Y<8+&w>XI7$BpQyW>yH?C@Ma>3e|~No(Fn&eu z-m}ZU%BB50jq&UL&9AB&R6WH~;;PxNBX?>{BcGZ!pL_n6SY}Y*&wUWf&3_?;r*mrdw(VXEKV} zM8zUs{QBh~ULoaaY!>@^ic}dZ>?qLRt#5vuYF(rCIfshfO#r{jN}-t4CXA;Qm;%s;yQ3``%f+VwK<2`p-BKMcl^=+)Xb1-J&Mj@YDKl)v^B;5tT9 z^9hEmgjwf86qWvUgyWo2&;<1F47j9-@^)5Z$4hQLq=^->#^6;0KGSyK>WPS)qskma z;dGe=Vyn%#9JVoITjxS?2X^1SRY+g>%mrU-NdwYmeXrd@Qv!QMI=8UMW;;iB`P64(lS@}l#@PSvOl3v4=J@(lteTOCFDcbol4-fw5c29~gUg8e zWPr!g6{@CB$wau*j#yV*fva6i0SHbff>JWWaGfTtHK)Jd`N0~~{Iw$95}doIgy!d! z)I!b=B13=2Bt`AG2ivY-k?}%b4Ze*@FCewX@bir6&=eg%R6g0Z{ka) zzQRmKkp1p0IB61V@oL^blZc(qCJ5xKzvm?56H`f2p4%jnTDwcs-3O5MKqdG6d)nP}2 zWQFMcqy`qf(QB(;-q>d=Y#Im7vlb_57G!q2SN2y9T0hgX?}?~x)A)K2A8>LU+VtRD z$pMxSLsfd+>;rZ`&mgrGnR2`f`p`yO634lnZ;ExUwS-Rv4^U)+UpY=!PHNTN?tD?0#~>3ArVlCZyD(mjE}sC(x10ltT@81G@cvz3;49sxd#&iQT|9 z7vCERsa%_`O1{(?$MfeD^k5Gml|p6X9eA^V*L-L9PKRCFaXfKEYW9@eSHH?phg~y^ zNx@SsZDTYl{;SOL+s6J(a*q+xfcQ1M!f)5y&f2L#*PL@Pvrg0onlv>z=TntRUJEA0 zXF&%&lG#Q61J8>}`dqV7MKyiU(Xw9p@?SIsoj8XY2c!uU=j=vuMQ3-}mEZLA)Q2x_ zThmqpXw!$%<{>Y#$(=d4fHL{r-S4+;ND?q5&1^*nUaz zC)Fk`W>ZCaSN|h{;&+j|I75hG4AKhYzhdoQr#CGgtAs6tP?Zf9D8 zCfWfZ=4_V|d1G}bdRBYGikSq!i{f$lz8EmyhcC$ zi=;2~LROO!GEgXx0R&zgeL7I{JKRufZsBpjzyAMlYgCG>Mi?e((9i5Ax9BOgNRRI< zPL`aBN-TEsMOtOjpuf2|;*DpoE}0h+&nqH{>2R<}n0bdU|@oNduj}xU4EA(fb|%T~+=szQE@R z@kRi2s@&F8?%mSrp0&?@eOa*103UcJ|BDpsC3ri-!v~iaMds#6HC8|>1#0tO0(kVH zZy^^)MJcofpvp$KP5eK8?f3a?G@^+)wwEC@1Rhd2BLk8{_D{D7Rk&79A>kl4U{Y8x z^%m0y3csrElh_*=X|EtylpP4u^DX{S)dKvF?`3+o?c#7!ro+`OuXRzxdFaZnX+tjT z2!{N3gi;%ppM2n_-<-CBx_Bhv$HVzQEnoz56IS*k2Vf3eZIPZsO2P-+t{wB!Dn~dk zlFCkY=2BM<3Ji^q1gtVjd_o=S0PTW?hQ?U*cVlcB5)N3r?O>unn2hj`o_A2%EIpIT z7W6y+EHr-c8=R^!xV`r8);Y~Sti6RN!DiMG!v!#4_`!RNt&1{FfYp69|59F?)AoBQ z-n(kRqPd2`-ElyJxR{u|^>ub~!24qXz!yN9wsSgr;jUQ{R_)$@iLed=8@2C9qDIL! zrdTnlTRfil=9WMd4-64Nl_e_Q%CpxLq8{w`I-pVW4>CqqULYCYf+t%F0qg~+GT)ur zDKbGvl^6UQd=p_U@IDkrgrrvH`=}${zLQAo+!A7BcylpP$S`1s9RAb<&ceAD|6P#U7^mSzGr)!CUsMCN7e%DbB~Y!4RNS z?edUh$RzrE3$E^a#MBP||BX9l2?w$erWyPAtQ<}~vH+GeCpVyJfnVYdvNW77iPdJj zi@F(NOVQO7^08!38{BNKf$U7R7>WYfwk$C(o~=jY-EqENdS&KiwkLEH9xH9CCXgut zmMWL&*bH2kDH!H0F!)v%SQJd(E0_M`GT!Ea-JVo3-V)UqN#U2CD&Sy-Bf2=|sJ9zv z%5~szrLlpc*W}6l;x?%^6b5rP4$_+=&`;LjQFs1Yoi6{d00^Y;VS~BI`lujw3R&HdHr?}{^;WzOYuvegR1YJR8a(kJ_zmP;CdX4VfE|yvyzITJj z^ys2MjqHG?qU|flI$nS=ik)eqU)ovN_A5{;+4t=xs=^8_>o30OzVItAuuNyISuDmL z?*pHJ-oY2$4PdpXPjX~$16Qm$g%3`@~sF?3Km zkti}_I{`prYo@S4sUC9S^nE#=)hH~|T>x|vez_lOGqvd@?A|uBH7v?WwZlh;kSnN8 z-{;rs6n0suI7-)(CwZ-PsM>y(z{xpKEOEI9i&*ctn2__*og1DRk4#9jfe<*EeC#b}Axx&>i`>)`HguEvpK=PSB%SU8LHoJE(Docgm zdqEZ#2jKKu#WHTT$%~iL%Om%IBwzV0Gu|1aitkzGVD&8=PS)hlkz0_b70Dbs!v&0q zbCNU5;(xzu5&CCMHOO1}(92FV<6kdp`j#`$!prMFa**y{Xz$G`WgW@qLx3~Eh4~|Kmjglt@BhW#TXt31 zw*R7Z$0VdAC#`fygLF44N+TVTA|Qx##{@(g2`P&%krGJ-6qH7~1eB1F+Q)qU>sjjy z>^;VQvEF%)f!;djb)DyN{4xlx8N2&ykvq3|ya6%KPn(nDjnuJihnR$@>J@pKS8pa$ zt$%%grQTMQ-wrC!UgpPJXi4WW7})y#eHX{xSt$@M;S;sD-iGc!zWQkWeO1#VkXv?P zODAtt|M1K5`#qOu?SbtHLK$*oUodc-AnZb?uGNX_;9EsUx}Fs#=~w^a_@z)ESj6he z`!k)4A|SvcDQ!>8#q#4wlqFUCThn{cGd>_cEZl{G=fkdv6e{>q@b zz)OUBXe1@$M&u##mhC_TFh&(jiT=v+)tGc94!II#HtKCJX_ha1ztyB!x(V9>eO*E( z6G4Oa9qot>I2Y%L{=!m_aZfK*PoT5;B^&f;5&u)sa)SP|oGJGE-{!fB5aicP%7p+M zpWa%90$th4U0#(mjl0>OJy&HXx!x4)7D%?hl=e-M*cbTIq2|%ha{Q^HSLbO2>JNOA z5#RUzvhu@pI@)#y+64!lK>T&dKdrefFS%dr>B5WRY}MrSwFva2LIt|%$BPH-6TN&4 zTu+HA&wW6~O5#85TAhb*A!Ljm-yrpwl;V1)_Vabb zv}#^TF_hjQo^%i&0YCdG~7I0hO+MG&Gt+<{&-zpMv zTFP2^EekLUWT+Wu0&?vkkUqDT0iQ(XpiBTZd+U}Dj+>2I@ z_g07gi5;7(ph{)dtXX1>iy&)3o#5Ub2y$Wm07kM3>}Y8j)bf@|oIh3S zXE-caDh)FwJmxy)4}Q8V_lCo$mEH4UbUg&_v~n47c!*7_ZT$!yHghBlzh)&&b9cjK zHd&Az^@LlT(!ZRO%O*%-V2`>0GDlUP{RS+YJ(+@CuGbOIA}QkfW5#!?H;S2ES=8SL zLBO)fq5#$Xciq3pu69?!cie2H#DN}o^A!58fG~Wq9u9u)YJ)6}6NAi^;*TYHLN5O% zdv(ZbhotZ`CCbuW(xifKniR$cL`cYI-_$OAMn-n+?dNZhdK#FNT7MD^0q)1lC_X`n zj6Au(+!K8Q-vVr%Xj~uOF=G0(BH!{+QrU9~U!svn#Ur96Dn;cF+*Vl+^X*ln?ce&X zk93m3ogrhtJ^kQ);+T6;uWaGSc3Za?RR|1SXb0X-gnm1A?e)*scG1_)$^B|`9V20u z@`dT}>M#ApvbhS0m#I%6{@XV8Kl8`rvW&v%8XTm~nljvlD6`!ASk89^tlG}Ne_Vum zg!bDH6_;$}&NJ|U5TN0W{`)L^F)`;;JQ`CC+9CTQMrz8uUyGK0llF(0U%bpXiQ{rJ z>f(Q?g!MOke*+#sLCDzzY8plv@edfGv|EWmL8*kd6qWciV;NXKwQ%SAhwV{+eAl!X z2RBZ@oaof z3Gc&P`I#k$GQBCb?ez`52M!f-J~?_smP)K;3|q?!9DQlr$)~B#hZ^`% zP1t`R)$!v`hgkX?qy6PnZ2_-$PhcBZf`Lu>O$vGr@hK4Mg;2%(;KRudvKVyg?VlK5 z$m>2u=XGP{4^E@UIv*1≤nYA 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%0Dj~33wH;B^QIAR7BVBc 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 C_%+l3 literal 88552 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 aco-jC&98Esyc^&*F!ItWkTOYQ|Nj6M;5U*0 diff --git a/packages/core/src/docs/data-model/__tests__/empty-snapshot.spec.ts b/packages/core/src/docs/data-model/__tests__/empty-snapshot.spec.ts new file mode 100644 index 0000000000..dbf14f1f58 --- /dev/null +++ b/packages/core/src/docs/data-model/__tests__/empty-snapshot.spec.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; +import { getEmptySnapshot } from '../empty-snapshot'; + +describe('getEmptySnapshot', () => { + it('uses comfortable paragraph spacing defaults for new docs', () => { + expect(getEmptySnapshot().body?.paragraphs?.[0].paragraphStyle).toEqual({ + spaceAbove: { v: 0 }, + lineSpacing: 1.5, + spaceBelow: { v: 8 }, + }); + }); +}); diff --git a/packages/core/src/docs/data-model/__tests__/utils.spec.ts b/packages/core/src/docs/data-model/__tests__/utils.spec.ts index a45399f1a1..043c75c0e4 100644 --- a/packages/core/src/docs/data-model/__tests__/utils.spec.ts +++ b/packages/core/src/docs/data-model/__tests__/utils.spec.ts @@ -35,6 +35,10 @@ describe('document data-model utils', () => { it: BooleanNumber.FALSE, bl: BooleanNumber.FALSE, }); + const fontStackStyle = getFontStyleString({ + ff: '"SF Mono", "Cascadia Code", Consolas, monospace', + fs: 12, + }); expect(defaultStyle).toEqual({ fontCache: `${DEFAULT_STYLES.fs}pt ${DEFAULT_STYLES.ff}`, @@ -48,6 +52,8 @@ describe('document data-model utils', () => { expect(superscriptStyle.fontSize).toBeCloseTo(7.8); expect(superscriptStyle.fontString).toContain(DEFAULT_FONTFACE_PLANE); expect(normalStyle.fontCache).toBe('normal normal 14pt "Ping Fang"'); + expect(fontStackStyle.fontFamily).toBe('"SF Mono", "Cascadia Code", Consolas, monospace'); + expect(fontStackStyle.fontString).toContain('"SF Mono", "Cascadia Code", Consolas, monospace'); }); it('should expose baseline offsets and convert text rotation', () => { diff --git a/packages/core/src/docs/data-model/empty-snapshot.ts b/packages/core/src/docs/data-model/empty-snapshot.ts index 2e85a75299..0e19d495a5 100644 --- a/packages/core/src/docs/data-model/empty-snapshot.ts +++ b/packages/core/src/docs/data-model/empty-snapshot.ts @@ -16,6 +16,7 @@ import type { IDocumentData } from '../../types/interfaces'; import { generateRandomId } from '../../shared/tools'; +import { DEFAULT_DOCUMENT_PARAGRAPH_LINE_SPACING, DEFAULT_DOCUMENT_PARAGRAPH_SPACE_ABOVE, DEFAULT_DOCUMENT_PARAGRAPH_SPACE_BELOW, MODERN_DOCUMENT_WIDTH, ModernDocumentWidthMode } from '../../types/const'; import { BooleanNumber } from '../../types/enum'; import { LocaleType } from '../../types/enum/locale-type'; import { DocumentFlavor } from '../../types/interfaces'; @@ -43,9 +44,9 @@ export function getEmptySnapshot( { startIndex: 0, paragraphStyle: { - spaceAbove: { v: 5 }, - lineSpacing: 1, - spaceBelow: { v: 0 }, + spaceAbove: { v: DEFAULT_DOCUMENT_PARAGRAPH_SPACE_ABOVE }, + lineSpacing: DEFAULT_DOCUMENT_PARAGRAPH_LINE_SPACING, + spaceBelow: { v: DEFAULT_DOCUMENT_PARAGRAPH_SPACE_BELOW }, }, }, ], @@ -57,10 +58,10 @@ export function getEmptySnapshot( }, documentStyle: { pageSize: { - width: 595 / 0.75, + width: MODERN_DOCUMENT_WIDTH[ModernDocumentWidthMode.MEDIUM], height: 842 / 0.75, }, - documentFlavor: DocumentFlavor.TRADITIONAL, + documentFlavor: DocumentFlavor.MODERN, marginTop: 50, marginBottom: 50, marginRight: 50, diff --git a/packages/core/src/docs/data-model/text-x/__tests__/block-ranges.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/block-ranges.spec.ts new file mode 100644 index 0000000000..556e9748e7 --- /dev/null +++ b/packages/core/src/docs/data-model/text-x/__tests__/block-ranges.spec.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDocumentBody } from '../../../../types/interfaces'; +import { describe, expect, it } from 'vitest'; +import { DataStreamTreeTokenType } from '../../types'; +import { deleteBlockRanges, insertBlockRanges } from '../apply-utils/common'; +import { getPlainText } from '../build-utils/parse'; +import { getBodySlice } from '../utils'; + +describe('document block ranges', () => { + it('moves following block ranges when text is inserted before them', () => { + const body: IDocumentBody = { + dataStream: 'A\rB\r\n', + blockRanges: [{ blockId: 'callout-1', blockType: 'callout', startIndex: 2, endIndex: 3 }], + }; + + insertBlockRanges(body, { dataStream: 'XX' }, 2, 1); + + expect(body.blockRanges).toEqual([ + { blockId: 'callout-1', blockType: 'callout', startIndex: 4, endIndex: 5 }, + ]); + }); + + it('expands containing block ranges when text is inserted inside them', () => { + const body: IDocumentBody = { + dataStream: `${DataStreamTreeTokenType.BLOCK_START}A\r${DataStreamTreeTokenType.BLOCK_END}\n`, + blockRanges: [{ blockId: 'callout-1', blockType: 'callout', startIndex: 0, endIndex: 3 }], + }; + + insertBlockRanges(body, { dataStream: 'XX' }, 2, 2); + + expect(body.blockRanges).toEqual([ + { blockId: 'callout-1', blockType: 'callout', startIndex: 0, endIndex: 5 }, + ]); + }); + + it('cuts block ranges to the copied body with relative indexes', () => { + const body: IDocumentBody = { + dataStream: `A\r${DataStreamTreeTokenType.BLOCK_START}B\r${DataStreamTreeTokenType.BLOCK_END}C\r\n`, + blockRanges: [{ blockId: 'callout-1', blockType: 'callout', startIndex: 2, endIndex: 5 }], + }; + + const slice = getBodySlice(body, 2, 6); + + expect(slice.dataStream).toBe(`${DataStreamTreeTokenType.BLOCK_START}B\r${DataStreamTreeTokenType.BLOCK_END}`); + expect(slice.blockRanges).toEqual([ + { blockId: 'callout-1', blockType: 'callout', startIndex: 0, endIndex: 3 }, + ]); + }); + + it('removes fully deleted block ranges', () => { + const body: IDocumentBody = { + dataStream: `${DataStreamTreeTokenType.BLOCK_START}A\r${DataStreamTreeTokenType.BLOCK_END}\n`, + blockRanges: [{ blockId: 'callout-1', blockType: 'callout', startIndex: 0, endIndex: 3 }], + }; + + const removed = deleteBlockRanges(body, 4, 0); + + expect(removed).toEqual([{ blockId: 'callout-1', blockType: 'callout', startIndex: 0, endIndex: 3 }]); + expect(body.blockRanges).toEqual([]); + }); + + it('removes block tokens from plain text', () => { + const stream = `${DataStreamTreeTokenType.BLOCK_START}Callout\r${DataStreamTreeTokenType.BLOCK_END}\n`; + + expect(getPlainText(stream)).toBe('Callout'); + }); +}); diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/common.ts b/packages/core/src/docs/data-model/text-x/apply-utils/common.ts index 2b9bedf703..fcc19fcc83 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/common.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/common.ts @@ -20,6 +20,7 @@ import type { ICustomDecoration, ICustomRange, ICustomTable, + IDocumentBlockRange, IDocumentBody, IParagraph, ISectionBreak, @@ -315,6 +316,41 @@ export function insertTables(body: IDocumentBody, insertBody: IDocumentBody, tex } } +export function insertBlockRanges(body: IDocumentBody, insertBody: IDocumentBody, textLength: number, currentIndex: number) { + if (!body.blockRanges && !insertBody.blockRanges?.length) { + return; + } + + if (!body.blockRanges) { + body.blockRanges = []; + } + + const { blockRanges } = body; + for (let i = 0, len = blockRanges.length; i < len; i++) { + const blockRange = blockRanges[i]; + const { startIndex, endIndex } = blockRange; + + if (startIndex >= currentIndex) { + blockRange.startIndex += textLength; + blockRange.endIndex += textLength; + } else if (endIndex >= currentIndex) { + blockRange.endIndex += textLength; + } + } + + const insertBlockRanges = insertBody.blockRanges; + if (insertBlockRanges) { + for (let i = 0, len = insertBlockRanges.length; i < len; i++) { + const blockRange = insertBlockRanges[i]; + blockRange.startIndex += currentIndex; + blockRange.endIndex += currentIndex; + } + + blockRanges.push(...insertBlockRanges); + blockRanges.sort(sortRulesFactory('startIndex')); + } +} + export function sliceByParagraph(body: IDocumentBody) { const { dataStream, paragraphs = [] } = body; const ranges = []; @@ -873,6 +909,44 @@ export function deleteCustomRanges(body: IDocumentBody, textLength: number, curr return removeCustomRanges; } +export function deleteBlockRanges(body: IDocumentBody, textLength: number, currentIndex: number) { + const { blockRanges } = body; + + const startIndex = currentIndex; + const endIndex = currentIndex + textLength - 1; + const removeBlockRanges: IDocumentBlockRange[] = []; + + if (blockRanges) { + const newBlockRanges = []; + for (let i = 0, len = blockRanges.length; i < len; i++) { + const blockRange = blockRanges[i]; + const { startIndex: st, endIndex: ed } = blockRange; + if (st >= startIndex && ed <= endIndex) { + removeBlockRanges.push(blockRange); + continue; + } else if (Math.max(startIndex, st) <= Math.min(endIndex, ed)) { + const segments = horizontalLineSegmentsSubtraction(st, ed, startIndex, endIndex); + + if (segments.length === 0) { + removeBlockRanges.push(blockRange); + continue; + } + + blockRange.startIndex = segments[0]; + blockRange.endIndex = segments[1]; + } else if (endIndex < st) { + blockRange.startIndex -= textLength; + blockRange.endIndex -= textLength; + } + newBlockRanges.push(blockRange); + } + + body.blockRanges = newBlockRanges; + } + + return removeBlockRanges; +} + export function deleteCustomDecorations(body: IDocumentBody, textLength: number, currentIndex: number, needOffset = true) { const { customDecorations } = body; diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts index d2a89b465e..edb18112f2 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts @@ -17,6 +17,7 @@ import type { IDocumentBody } from '../../../../types/interfaces'; import { deleteContent } from '../../../../shared'; import { + deleteBlockRanges, deleteCustomBlocks, deleteCustomDecorations, deleteCustomRanges, @@ -42,6 +43,8 @@ export function updateAttributeByDelete(body: IDocumentBody, textLength: number, const removeTables = deleteTables(body, textLength, currentIndex); + const removeBlockRanges = deleteBlockRanges(body, textLength, currentIndex); + const removeCustomRanges = deleteCustomRanges(body, textLength, currentIndex); const removeCustomDecorations = deleteCustomDecorations(body, textLength, currentIndex); @@ -59,6 +62,7 @@ export function updateAttributeByDelete(body: IDocumentBody, textLength: number, sectionBreaks: removeSectionBreaks, customBlocks: removeCustomBlocks, tables: removeTables, + blockRanges: removeBlockRanges, customRanges: removeCustomRanges, customDecorations: removeCustomDecorations, }; diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts index 8225463aae..357439963a 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts @@ -17,6 +17,7 @@ import type { IDocumentBody } from '../../../../types/interfaces'; import { insertTextToContent } from '../../../../shared'; import { + insertBlockRanges, insertCustomBlocks, insertCustomDecorations, insertCustomRanges, @@ -44,6 +45,8 @@ export function updateAttributeByInsert( insertTables(body, insertBody, textLength, currentIndex); + insertBlockRanges(body, insertBody, textLength, currentIndex); + insertCustomRanges(body, insertBody, textLength, currentIndex); insertCustomDecorations(body, insertBody, textLength, currentIndex); diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts index 4012ba7390..ae9019a86e 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts @@ -20,11 +20,13 @@ import { Tools, UpdateDocsAttributeType } from '../../../../shared'; import { CustomDecorationType } from '../../../../types/interfaces'; import { PresetListType } from '../../preset-list-type'; import { + deleteBlockRanges, deleteCustomBlocks, deleteParagraphs, deleteSectionBreaks, deleteTables, deleteTextRuns, + insertBlockRanges, insertCustomBlocks, insertParagraphs, insertSectionBreaks, @@ -49,6 +51,7 @@ export function updateAttribute( const removeSectionBreaks = updateSectionBreaks(body, updateBody, textLength, currentIndex, coverType); const removeCustomBlocks = updateCustomBlocks(body, updateBody, textLength, currentIndex, coverType); const removeTables = updateTables(body, updateBody, textLength, currentIndex, coverType); + const removeBlockRanges = updateBlockRanges(body, updateBody, textLength, currentIndex, coverType); const removeCustomRanges = updateCustomRanges(body, updateBody, textLength, currentIndex, coverType); const removeCustomDecorations = updateCustomDecorations(body, updateBody, textLength, currentIndex, coverType); @@ -59,6 +62,7 @@ export function updateAttribute( sectionBreaks: removeSectionBreaks, customBlocks: removeCustomBlocks, tables: removeTables, + blockRanges: removeBlockRanges, customRanges: removeCustomRanges, customDecorations: removeCustomDecorations, }; @@ -433,6 +437,33 @@ function updateTables( return removeTables; } +function updateBlockRanges( + body: IDocumentBody, + updateBody: IDocumentBody, + textLength: number, + currentIndex: number, + coverType: UpdateDocsAttributeType +) { + const { blockRanges } = body; + const { blockRanges: updateDataBlockRanges } = updateBody; + + if (blockRanges == null || updateDataBlockRanges == null) { + return; + } + + const removeBlockRanges = deleteBlockRanges(body, textLength, currentIndex); + if (coverType !== UpdateDocsAttributeType.REPLACE) { + updateBody.blockRanges = updateDataBlockRanges.map((updateBlockRange) => { + const removeBlockRange = removeBlockRanges.find((blockRange) => blockRange.blockId === updateBlockRange.blockId); + return removeBlockRange ? { ...removeBlockRange, ...updateBlockRange } : updateBlockRange; + }); + } + + insertBlockRanges(body, updateBody, textLength, currentIndex); + + return removeBlockRanges; +} + // retain function updateCustomRanges( body: IDocumentBody, diff --git a/packages/core/src/docs/data-model/text-x/build-utils/parse.ts b/packages/core/src/docs/data-model/text-x/build-utils/parse.ts index 043a23ae90..a15973ffc7 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/parse.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/parse.ts @@ -28,6 +28,8 @@ const tags = [ DataStreamTreeTokenType.TABLE_CELL_END, // table start DataStreamTreeTokenType.TABLE_ROW_END, // table start DataStreamTreeTokenType.TABLE_END, // table end + DataStreamTreeTokenType.BLOCK_START, // block start + DataStreamTreeTokenType.BLOCK_END, // block end // DataStreamTreeTokenType.COLUMN_BREAK, // column break // DataStreamTreeTokenType.PAGE_BREAK, // page break // DataStreamTreeTokenType.DOCS_END, // document end @@ -36,8 +38,8 @@ const tags = [ ]; export const getPlainText = (dataStream: string) => { - const text = dataStream.endsWith('\r\n') ? dataStream.slice(0, -2) : dataStream; - return tags.reduce((res, curr) => res.replaceAll(curr, ''), text); + const text = tags.reduce((res, curr) => res.replaceAll(curr, ''), dataStream); + return text.endsWith('\r\n') ? text.slice(0, -2) : text; }; export const isEmptyDocument = (dataStream?: string) => { diff --git a/packages/core/src/docs/data-model/text-x/transform-utils.ts b/packages/core/src/docs/data-model/text-x/transform-utils.ts index 6cb4be1e06..f2b3f9c36a 100644 --- a/packages/core/src/docs/data-model/text-x/transform-utils.ts +++ b/packages/core/src/docs/data-model/text-x/transform-utils.ts @@ -17,7 +17,7 @@ /* eslint-disable no-param-reassign */ import type { Nullable } from '../../../shared'; -import type { ICustomDecoration, ICustomRange, IDocumentBody, IParagraph, IParagraphStyle, ITextRun, ITextStyle } from '../../../types/interfaces'; +import type { ICustomDecoration, ICustomRange, IDocumentBlockRange, IDocumentBody, IParagraph, IParagraphStyle, ITextRun, ITextStyle } from '../../../types/interfaces'; import type { IRetainAction } from './action-types'; import { Tools, UpdateDocsAttributeType } from '../../../shared'; import { CustomDecorationType } from '../../../types/interfaces'; @@ -422,12 +422,14 @@ export function transformBody( const { textRuns: thisTextRuns, paragraphs: thisParagraphs = [], + blockRanges: thisBlockRanges = [], customRanges: thisCustomRanges, customDecorations: thisCustomDecorations, } = thisBody; const { textRuns: otherTextRuns, paragraphs: otherParagraphs = [], + blockRanges: otherBlockRanges = [], customRanges: otherCustomRanges, customDecorations: otherCustomDecorations, } = otherBody; @@ -521,8 +523,28 @@ export function transformBody( retBody.paragraphs = paragraphs; } + const blockRanges = transformBlockRanges(thisBlockRanges, otherBlockRanges, priority); + if (blockRanges.length) { + retBody.blockRanges = blockRanges; + } + return { coverType, body: retBody, }; } + +function transformBlockRanges(thisBlockRanges: IDocumentBlockRange[], otherBlockRanges: IDocumentBlockRange[], priority: boolean): IDocumentBlockRange[] { + if (!thisBlockRanges.length) { + return otherBlockRanges; + } + + if (!otherBlockRanges.length) { + return []; + } + + return otherBlockRanges.map((otherBlockRange) => { + const thisBlockRange = thisBlockRanges.find((blockRange) => blockRange.blockId === otherBlockRange.blockId); + return thisBlockRange && priority ? Tools.deepMerge(otherBlockRange, thisBlockRange) : otherBlockRange; + }); +} diff --git a/packages/core/src/docs/data-model/text-x/utils.ts b/packages/core/src/docs/data-model/text-x/utils.ts index dfa71ef493..c13417f317 100644 --- a/packages/core/src/docs/data-model/text-x/utils.ts +++ b/packages/core/src/docs/data-model/text-x/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ICustomBlock, ICustomDecoration, ICustomRange, IDocumentBody, IParagraph, ISectionBreak, ITextRun } from '../../../types/interfaces/i-document-data'; +import type { ICustomBlock, ICustomDecoration, ICustomRange, IDocumentBlockRange, IDocumentBody, IParagraph, ISectionBreak, ITextRun } from '../../../types/interfaces/i-document-data'; import type { IRetainAction } from './action-types'; import { UpdateDocsAttributeType } from '../../../shared/command-enum'; import { Tools } from '../../../shared/tools'; @@ -103,6 +103,30 @@ export function getTableSlice( return newTables; } +export function getBlockRangeSlice( + body: IDocumentBody, + startOffset: number, + endOffset: number +) { + const { blockRanges = [] } = body; + const newBlockRanges: IDocumentBlockRange[] = []; + + for (const blockRange of blockRanges) { + const clonedBlockRange = Tools.deepClone(blockRange); + const { startIndex, endIndex } = clonedBlockRange; + + if (startIndex >= startOffset && endIndex < endOffset) { + newBlockRanges.push({ + ...clonedBlockRange, + startIndex: startIndex - startOffset, + endIndex: endIndex - startOffset, + }); + } + } + + return newBlockRanges; +} + export function getParagraphsSlice( body: IDocumentBody, startOffset: number, @@ -193,6 +217,11 @@ export function getBodySlice( docBody.tables = newTables; } + const newBlockRanges = getBlockRangeSlice(body, startOffset, endOffset); + if (newBlockRanges.length) { + docBody.blockRanges = newBlockRanges; + } + docBody.paragraphs = getParagraphsSlice(body, startOffset, endOffset); if (type === SliceBodyType.cut) { @@ -216,7 +245,7 @@ export function getBodySlice( } export function normalizeBody(body: IDocumentBody): IDocumentBody { - const { dataStream, textRuns, paragraphs, customRanges, customDecorations, tables } = body; + const { dataStream, textRuns, paragraphs, customRanges, customDecorations, tables, blockRanges } = body; let leftOffset = 0; let rightOffset = 0; @@ -274,6 +303,7 @@ export function normalizeBody(body: IDocumentBody): IDocumentBody { customRanges, customDecorations, tables, + blockRanges, }; } @@ -409,12 +439,14 @@ export function composeBody( paragraphs: thisParagraphs = [], customRanges: thisCustomRanges, customDecorations: thisCustomDecorations = [], + blockRanges: thisBlockRanges = [], } = thisBody; const { textRuns: otherTextRuns, paragraphs: otherParagraphs = [], customRanges: otherCustomRanges, customDecorations: otherCustomDecorations = [], + blockRanges: otherBlockRanges = [], } = otherBody; retBody.textRuns = composeTextRuns(otherTextRuns, thisTextRuns, coverType); @@ -463,9 +495,32 @@ export function composeBody( retBody.paragraphs = paragraphs; } + const blockRanges = composeDocumentBlockRanges(thisBlockRanges, otherBlockRanges); + if (blockRanges.length) { + retBody.blockRanges = blockRanges; + } + return retBody; } +function composeDocumentBlockRanges( + thisRanges: IDocumentBlockRange[], + otherRanges: IDocumentBlockRange[] +): IDocumentBlockRange[] { + if (!thisRanges.length) { + return otherRanges; + } + + if (!otherRanges.length) { + return thisRanges; + } + + const byId = new Map(thisRanges.map((range) => [range.blockId, Tools.deepClone(range)])); + otherRanges.forEach((range) => byId.set(range.blockId, Tools.deepClone(range))); + + return Array.from(byId.values()).sort((left, right) => left.startIndex - right.startIndex); +} + export function isUselessRetainAction(action: IRetainAction): boolean { const { body } = action; @@ -473,9 +528,9 @@ export function isUselessRetainAction(action: IRetainAction): boolean { return true; } - const { textRuns, paragraphs, customRanges, customBlocks, customDecorations, tables } = body; + const { textRuns, paragraphs, customRanges, customBlocks, customDecorations, tables, blockRanges } = body; - if (textRuns == null && paragraphs == null && customRanges == null && customBlocks == null && customDecorations == null && tables == null) { + if (textRuns == null && paragraphs == null && customRanges == null && customBlocks == null && customDecorations == null && tables == null && blockRanges == null) { return true; } diff --git a/packages/core/src/docs/data-model/types.ts b/packages/core/src/docs/data-model/types.ts index 997dfea70f..8836e2b3cd 100644 --- a/packages/core/src/docs/data-model/types.ts +++ b/packages/core/src/docs/data-model/types.ts @@ -24,6 +24,7 @@ export enum DataStreamTreeNodeType { TABLE = 'TABLE', TABLE_ROW = 'TABLE_ROW', TABLE_CELL = 'TABLE_CELL', + BLOCK = 'BLOCK', CUSTOM_BLOCK = 'CUSTOM_BLOCK', // \b images, mentions, etc. that do not participate in document flow } @@ -37,6 +38,9 @@ export enum DataStreamTreeTokenType { TABLE_CELL_END = '\x1D', // table cell end TABLE_ROW_END = '\x0E', // table row end TABLE_END = '\x0F', // table end + // block. + BLOCK_START = '\x10', // block start + BLOCK_END = '\x11', // block end // custom range. /** * @deprecated diff --git a/packages/core/src/docs/data-model/utils.ts b/packages/core/src/docs/data-model/utils.ts index 6a326f082e..f8a8f3edda 100644 --- a/packages/core/src/docs/data-model/utils.ts +++ b/packages/core/src/docs/data-model/utils.ts @@ -31,7 +31,6 @@ export interface IDocumentSkeletonFontStyle { fontCache: string; } -// eslint-disable-next-line max-lines-per-function export function getFontStyleString( textStyle?: IStyleBase ): IDocumentSkeletonFontStyle { @@ -72,26 +71,7 @@ export function getFontStyleString( originFontSize = Math.ceil(textStyle.fs); } - let fontFamilyResult = defaultFont; - if (textStyle.ff) { - let fontFamily = textStyle.ff; - - fontFamily = fontFamily.replace(/"/g, '').replace(/'/g, ''); - - if (fontFamily.indexOf(' ') > -1) { - fontFamily = `"${fontFamily}"`; - } - - // if (fontFamily != null && document.fonts && !document.fonts.check('12px ' + fontFamily)) { - // menuButton.addFontToList(fontFamily); - // } - - if (fontFamily == null) { - fontFamily = defaultFont; - } - - fontFamilyResult = fontFamily; - } + const fontFamilyResult = normalizeFontFamily(textStyle.ff, defaultFont); const { va: baselineOffset } = textStyle; @@ -119,6 +99,21 @@ export function getFontStyleString( }; } +function normalizeFontFamily(fontFamily: string | null | undefined | void, defaultFont: string): string { + if (!fontFamily?.trim()) { + return defaultFont; + } + + return fontFamily + .split(',') + .map((item) => { + const family = item.trim().replace(/^['"]|['"]$/g, ''); + return family.includes(' ') ? `"${family}"` : family; + }) + .filter(Boolean) + .join(', '); +} + export function getBaselineOffsetInfo(_fontFamily: string, fontSize: number) { // The origin FontCache.getBaselineOffsetInfo needs _fontDataMap // But now _fontDataMap didn't not set value. So we can use getDefault. diff --git a/packages/core/src/sheets/__tests__/sheet-skeleton.integration.spec.ts b/packages/core/src/sheets/__tests__/sheet-skeleton.integration.spec.ts index 7f9a586cba..01913ece21 100644 --- a/packages/core/src/sheets/__tests__/sheet-skeleton.integration.spec.ts +++ b/packages/core/src/sheets/__tests__/sheet-skeleton.integration.spec.ts @@ -25,6 +25,7 @@ import { ThemeService } from '../../services/theme/theme.service'; import { ColorKit } from '../../shared'; import { BooleanNumber, HorizontalAlign } from '../../types/enum'; import { LocaleType } from '../../types/enum/locale-type'; +import { DocumentFlavor } from '../../types/interfaces'; import { createSheetGapTestConfig, SheetSkeleton } from '../sheet-skeleton'; import { RANGE_TYPE } from '../typedef'; import { createCoreTestBed } from './create-core-test-bed'; @@ -271,6 +272,8 @@ describe('SheetSkeleton integration', () => { width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY, }, + documentFlavor: DocumentFlavor.UNSPECIFIED, + paragraphLineGapDefault: 0, renderConfig: { horizontalAlign: HorizontalAlign.CENTER, }, diff --git a/packages/core/src/sheets/__tests__/util.spec.ts b/packages/core/src/sheets/__tests__/util.spec.ts index 220ce3db35..e387634c52 100644 --- a/packages/core/src/sheets/__tests__/util.spec.ts +++ b/packages/core/src/sheets/__tests__/util.spec.ts @@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'; import { BaselineOffset, BooleanNumber, HorizontalAlign, VerticalAlign, WrapStrategy } from '../../types/enum'; -import { CustomRangeType } from '../../types/interfaces'; +import { CustomRangeType, DocumentFlavor } from '../../types/interfaces'; import { addLinkToDocumentModel, createDocumentModelWithStyle, @@ -48,6 +48,8 @@ describe('sheet util helpers', () => { marginRight: 4, marginBottom: 3, marginLeft: 2, + documentFlavor: DocumentFlavor.UNSPECIFIED, + paragraphLineGapDefault: 0, renderConfig: { horizontalAlign: HorizontalAlign.CENTER, verticalAlign: VerticalAlign.MIDDLE, diff --git a/packages/core/src/sheets/sheet-skeleton.ts b/packages/core/src/sheets/sheet-skeleton.ts index 402724c8a8..43d76255c7 100644 --- a/packages/core/src/sheets/sheet-skeleton.ts +++ b/packages/core/src/sheets/sheet-skeleton.ts @@ -46,6 +46,7 @@ import { ImageCacheMap } from '../shared/cache/image-cache'; import { getIntersectRange } from '../shared/range'; import { Skeleton } from '../skeleton'; import { BooleanNumber, HorizontalAlign } from '../types/enum'; +import { DocumentFlavor } from '../types/interfaces'; /** * Configuration for a single gap (visual separator between rows or columns). @@ -1174,6 +1175,8 @@ export class SheetSkeleton extends Skeleton { width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY, }; + documentData.documentStyle.documentFlavor = DocumentFlavor.UNSPECIFIED; + documentData.documentStyle.paragraphLineGapDefault = 0; documentData.documentStyle.renderConfig = { ...documentData.documentStyle.renderConfig, diff --git a/packages/core/src/sheets/util.ts b/packages/core/src/sheets/util.ts index 4887e9547b..cf9d5b3cbb 100644 --- a/packages/core/src/sheets/util.ts +++ b/packages/core/src/sheets/util.ts @@ -24,7 +24,7 @@ import { TextX } from '../docs/data-model/text-x/text-x'; import { convertTextRotation } from '../docs/data-model/utils'; import { Rectangle } from '../shared'; import { HorizontalAlign, VerticalAlign, WrapStrategy } from '../types/enum'; -import { CustomRangeType } from '../types/interfaces'; +import { CustomRangeType, DocumentFlavor } from '../types/interfaces'; export interface IFontLocale { fontList: string[]; @@ -109,10 +109,12 @@ export function createDocumentModelWithStyle(content: string, textStyle: ITextSt width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY, }, + documentFlavor: DocumentFlavor.UNSPECIFIED, marginTop, marginBottom, marginRight, marginLeft, + paragraphLineGapDefault: 0, renderConfig: { horizontalAlign, verticalAlign, diff --git a/packages/core/src/sheets/worksheet.ts b/packages/core/src/sheets/worksheet.ts index 56ee72a23f..8989f1f445 100644 --- a/packages/core/src/sheets/worksheet.ts +++ b/packages/core/src/sheets/worksheet.ts @@ -28,6 +28,7 @@ import { createRowColIter } from '../shared/row-col-iter'; import { generateRandomId } from '../shared/tools'; import { DEFAULT_STYLES } from '../types/const'; import { CellValueType } from '../types/enum'; +import { DocumentFlavor } from '../types/interfaces'; import { cloneWorksheetData } from './clone'; import { ColumnManager } from './column-manager'; import { Range } from './range'; @@ -1269,6 +1270,8 @@ export class Worksheet { width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY, }; + documentData.documentStyle.documentFlavor = DocumentFlavor.UNSPECIFIED; + documentData.documentStyle.paragraphLineGapDefault = 0; documentData.documentStyle.renderConfig = { ...documentData.documentStyle.renderConfig, diff --git a/packages/core/src/types/const/const.ts b/packages/core/src/types/const/const.ts index aa037ab67d..6925511f53 100644 --- a/packages/core/src/types/const/const.ts +++ b/packages/core/src/types/const/const.ts @@ -184,6 +184,10 @@ export const NAMED_STYLE_MAP: Record> = { const BOTTOM_P = 4; +export const DEFAULT_DOCUMENT_PARAGRAPH_LINE_SPACING = 1.5; +export const DEFAULT_DOCUMENT_PARAGRAPH_SPACE_ABOVE = 0; +export const DEFAULT_DOCUMENT_PARAGRAPH_SPACE_BELOW = 8; + export const NAMED_STYLE_SPACE_MAP: Record> = { [NamedStyleType.HEADING_1]: { spaceAbove: { @@ -227,10 +231,10 @@ export const NAMED_STYLE_SPACE_MAP: Record> = { height: 1632, }, }; + +export enum ModernDocumentWidthMode { + NARROW = 'narrow', + MEDIUM = 'medium', + WIDE = 'wide', +} + +export const MODERN_DOCUMENT_WIDTH: Record = { + [ModernDocumentWidthMode.NARROW]: PAGE_SIZE[PaperType.A4].width, + [ModernDocumentWidthMode.MEDIUM]: 960, + [ModernDocumentWidthMode.WIDE]: PAGE_SIZE[PaperType.A3].width, +}; + +export const MODERN_DOCUMENT_DEFAULT_MARGIN = 50 / 0.75; diff --git a/packages/core/src/types/interfaces/i-document-data.ts b/packages/core/src/types/interfaces/i-document-data.ts index b595c22c95..c3dfce0298 100644 --- a/packages/core/src/types/interfaces/i-document-data.ts +++ b/packages/core/src/types/interfaces/i-document-data.ts @@ -131,6 +131,8 @@ export interface IDocumentBody { tables?: ICustomTable[]; // Table + blockRanges?: IDocumentBlockRange[]; // Generic structured block range, e.g. callout, quote, code. + // tableOfContents?: { [index: number]: ITableOfContent }; // tableOfContents // links?: { [index: number]: IHyperlink }; // links @@ -363,6 +365,13 @@ export type IHyperLinkCustomRange = ICustomRange<{ url: string }>; export type IMentionCustomRange = ICustomRange; +export interface IDocumentBlockRange { + startIndex: number; + endIndex: number; + blockId: string; + blockType: string; +} + export enum CustomRangeType { HYPERLINK, FIELD, // 17.16 Fields and Hyperlinks diff --git a/packages/docs-drawing-ui/src/commands/commands/__tests__/drawing-commands.spec.ts b/packages/docs-drawing-ui/src/commands/commands/__tests__/drawing-commands.spec.ts index 371ddb05cd..58e8ced678 100644 --- a/packages/docs-drawing-ui/src/commands/commands/__tests__/drawing-commands.spec.ts +++ b/packages/docs-drawing-ui/src/commands/commands/__tests__/drawing-commands.spec.ts @@ -20,15 +20,17 @@ import { Direction, ICommandService, IUniverInstanceService, + ObjectRelativeFromH, + ObjectRelativeFromV, PositionedObjectLayoutType, UniverInstanceType, WrapTextType, } from '@univerjs/core'; -import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { DocSelectionManagerService, DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { DocDrawingController as CoreDocDrawingController, DocDrawingService, IDocDrawingService } from '@univerjs/docs-drawing'; -import { DocSelectionRenderService } from '@univerjs/docs-ui'; +import { DocContentInsertService, DocSelectionRenderService } from '@univerjs/docs-ui'; import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing'; -import { IRenderManagerService } from '@univerjs/engine-render'; +import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createDocUiTestBed } from '../../../__tests__/create-doc-ui-test-bed'; import { DocDrawingAddRemoveController } from '../../../controllers/doc-drawing-notification.controller'; @@ -36,7 +38,7 @@ import { DeleteDocDrawingsCommand } from '../delete-doc-drawing.command'; import { InsertDocDrawingCommand } from '../insert-doc-drawing.command'; import { MoveDocDrawingsCommand } from '../move-drawings.command'; import { RemoveDocDrawingCommand } from '../remove-doc-drawing.command'; -import { UpdateDocDrawingDistanceCommand, UpdateDocDrawingWrapTextCommand, UpdateDrawingDocTransformCommand } from '../update-doc-drawing.command'; +import { TextWrappingStyle, UpdateDocDrawingDistanceCommand, UpdateDocDrawingWrappingStyleCommand, UpdateDocDrawingWrapTextCommand, UpdateDrawingDocTransformCommand } from '../update-doc-drawing.command'; function createBaseDocData(): IDocumentData { return { @@ -127,6 +129,7 @@ function setupDrawingTestBed(docData: IDocumentData) { injector.add([DocDrawingService]); injector.add([IDocDrawingService, { useClass: DocDrawingService }]); injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]); + injector.add([DocContentInsertService]); injector.add([CoreDocDrawingController]); injector.add([DocDrawingAddRemoveController]); @@ -137,6 +140,7 @@ function setupDrawingTestBed(docData: IDocumentData) { DeleteDocDrawingsCommand, MoveDocDrawingsCommand, UpdateDocDrawingDistanceCommand, + UpdateDocDrawingWrappingStyleCommand, UpdateDocDrawingWrapTextCommand, UpdateDrawingDocTransformCommand, RichTextEditingMutation as unknown as ICommand, @@ -211,6 +215,48 @@ describe('docs drawing commands integration', () => { testBed.univer.dispose(); }); + it('uses the explicit content insert range for drawing insertion', async () => { + const insertOffset = 5; + const testBed = setupDrawingTestBed(createBaseDocData()); + + testBed.selectionManager.__TEST_ONLY_add([{ + startOffset: 0, + endOffset: 0, + collapsed: true, + isActive: true, + segmentId: '', + style: null as never, + }]); + testBed.get(DocContentInsertService).setInsertRange({ + unitId: 'test-doc', + startOffset: insertOffset, + endOffset: insertOffset, + }); + + expect(await testBed.commandService.executeCommand(InsertDocDrawingCommand.id, { + drawings: [{ + drawingId: 'shape-1', + unitId: 'test-doc', + subUnitId: 'test-doc', + drawingType: 'image', + layoutType: PositionedObjectLayoutType.WRAP_SQUARE, + docTransform: { + positionH: { posOffset: 1 }, + positionV: { posOffset: 2 }, + }, + }], + })).toBe(true); + await awaitTime(0); + + const doc = testBed.get(IUniverInstanceService) + .getUnit('test-doc', UniverInstanceType.UNIVER_DOC)!; + + expect(doc.getBody()?.dataStream).toBe('Hello\b\r\n'); + expect(doc.getBody()?.customBlocks).toEqual([{ startIndex: insertOffset, blockId: 'shape-1' }]); + + testBed.univer.dispose(); + }); + it('deletes a focused drawing through the command pipeline and removes it from the document and services', async () => { const testBed = setupDrawingTestBed(createDrawingDocData()); @@ -303,6 +349,96 @@ describe('docs drawing commands integration', () => { testBed.univer.dispose(); }); + it('keeps table cell drawing position when switching from inline to floating layout', async () => { + const docData = createDrawingDocData(); + docData.drawings!['shape-1'].layoutType = PositionedObjectLayoutType.INLINE; + docData.drawings!['shape-1'].docTransform = { + size: { + width: 40, + height: 24, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 999, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PARAGRAPH, + posOffset: 999, + }, + angle: 0, + } as never; + const testBed = setupDrawingTestBed(docData); + const skeletonManager = testBed.injector.get(DocSkeletonManagerService); + vi.spyOn(skeletonManager, 'getViewModel').mockReturnValue({ + getEditArea: () => DocumentEditArea.BODY, + reset: vi.fn(), + } as never); + vi.spyOn(skeletonManager, 'getSkeleton').mockReturnValue({ + getSkeletonData: () => ({ + pages: [{ + marginTop: 72, + marginLeft: 90, + marginBottom: 72, + pageHeight: 800, + pageWidth: 600, + headerId: '', + footerId: '', + skeDrawings: new Map(), + skeTables: new Map([ + ['table-1', { + rows: [{ + cells: [{ + marginTop: 4, + marginLeft: 5, + skeDrawings: new Map([ + ['shape-1', { + drawingId: 'shape-1', + aLeft: 12, + aTop: 20, + columnLeft: 3, + lineTop: 7, + blockAnchorTop: 15, + drawingOrigin: docData.drawings!['shape-1'], + }], + ]), + skeTables: new Map(), + }], + }], + }], + ]), + }], + skeHeaders: new Map(), + skeFooters: new Map(), + }), + } as never); + + expect(await testBed.commandService.executeCommand(UpdateDocDrawingWrappingStyleCommand.id, { + unitId: 'test-doc', + subUnitId: 'test-doc', + drawings: [{ + drawingId: 'shape-1', + }], + wrappingStyle: TextWrappingStyle.WRAP_SQUARE, + })).toBe(true); + await awaitTime(0); + + const doc = testBed.get(IUniverInstanceService) + .getUnit('test-doc', UniverInstanceType.UNIVER_DOC)!; + const drawing = doc.getSnapshot().drawings?.['shape-1']; + + expect(drawing?.layoutType).toBe(PositionedObjectLayoutType.WRAP_SQUARE); + expect(drawing?.docTransform.positionH).toEqual({ + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 12, + }); + expect(drawing?.docTransform.positionV).toEqual({ + relativeFrom: ObjectRelativeFromV.PARAGRAPH, + posOffset: 5, + }); + + testBed.univer.dispose(); + }); + it('updates drawing doc transform through the command pipeline', async () => { const testBed = setupDrawingTestBed(createDrawingDocData()); diff --git a/packages/docs-drawing-ui/src/commands/commands/insert-doc-drawing.command.ts b/packages/docs-drawing-ui/src/commands/commands/insert-doc-drawing.command.ts index 8d38f82c95..600199f327 100644 --- a/packages/docs-drawing-ui/src/commands/commands/insert-doc-drawing.command.ts +++ b/packages/docs-drawing-ui/src/commands/commands/insert-doc-drawing.command.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { IAccessor, ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; +import type { IAccessor, ICommand, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IInsertDrawingCommandParams } from './interfaces'; import { @@ -27,7 +27,7 @@ import { TextXActionType, } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; -import { getCustomBlockIdsInSelections, getRichTextEditPath } from '@univerjs/docs-ui'; +import { DocContentInsertService, getCustomBlockIdsInSelections, getRichTextEditPath } from '@univerjs/docs-ui'; /** * The command to insert new drawings @@ -48,13 +48,28 @@ export const InsertDocDrawingCommand: ICommand = { const activeTextRange = docSelectionManagerService.getActiveTextRange(); const documentDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (activeTextRange == null || documentDataModel == null) { + if (documentDataModel == null) { return false; } const unitId = documentDataModel.getUnitId(); + const contentInsertRange = getContentInsertRange(accessor, unitId); + const targetTextRange = contentInsertRange + ? { + ...activeTextRange, + startOffset: contentInsertRange.startOffset, + endOffset: contentInsertRange.endOffset, + collapsed: contentInsertRange.startOffset === contentInsertRange.endOffset, + segmentId: contentInsertRange.segmentId ?? activeTextRange?.segmentId ?? '', + } + : activeTextRange; + + if (targetTextRange == null) { + return false; + } + const { drawings } = params; - const { collapsed, startOffset, segmentId } = activeTextRange; + const { collapsed, startOffset, segmentId = '' } = targetTextRange; const body = documentDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); if (body == null) { @@ -76,10 +91,10 @@ export const InsertDocDrawingCommand: ICommand = { }); } } else { - const dos = BuildTextUtils.selection.delete([activeTextRange], body, 0, null, false); + const dos = BuildTextUtils.selection.delete([targetTextRange], body, 0, null, false); textX.push(...dos); - const removedCustomBlockIds = getCustomBlockIdsInSelections(body, [activeTextRange]); + const removedCustomBlockIds = getCustomBlockIdsInSelections(body, [targetTextRange]); const drawings = documentDataModel.getDrawings() ?? {}; const drawingOrder = documentDataModel.getDrawingsOrder() ?? []; const sortedRemovedCustomBlockIds = removedCustomBlockIds.sort((a, b) => { @@ -159,3 +174,21 @@ export const InsertDocDrawingCommand: ICommand = { return Boolean(result); }, }; + +function getContentInsertRange(accessor: IAccessor, unitId: string): ITextRangeParam | null { + try { + const range = accessor.get(DocContentInsertService).consumeInsertRange(unitId); + if (range == null) { + return null; + } + + return { + startOffset: range.startOffset, + endOffset: range.endOffset, + collapsed: range.startOffset === range.endOffset, + segmentId: range.segmentId, + }; + } catch { + return null; + } +} diff --git a/packages/docs-drawing-ui/src/commands/commands/update-doc-drawing.command.ts b/packages/docs-drawing-ui/src/commands/commands/update-doc-drawing.command.ts index 414c179a38..43e261c0fb 100644 --- a/packages/docs-drawing-ui/src/commands/commands/update-doc-drawing.command.ts +++ b/packages/docs-drawing-ui/src/commands/commands/update-doc-drawing.command.ts @@ -17,6 +17,7 @@ import type { DocumentDataModel, IAccessor, ICommand, IDocDrawingBase, IDocDrawingPosition, IMutationInfo, IObjectPositionH, IObjectPositionV, ISize, JSONXActions, WrapTextType } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IDocDrawing } from '@univerjs/docs-drawing'; +import type { IDocumentSkeletonDrawing, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage } from '@univerjs/engine-render'; import { BooleanNumber, CommandType, @@ -51,6 +52,41 @@ const WRAPPING_STYLE_TO_LAYOUT_TYPE = { [TextWrappingStyle.BEHIND_TEXT]: PositionedObjectLayoutType.WRAP_NONE, }; +interface IDrawingAnchorInPage { + skeDrawing: IDocumentSkeletonDrawing; + pageMarginTop: number; + pageMarginLeft: number; +} + +export function findDrawingAnchorInPage( + page: IDocumentSkeletonPage | IDocumentSkeletonHeaderFooter, + drawingId: string, + pageMarginTop: number, + pageMarginLeft: number +): IDrawingAnchorInPage | null { + const skeDrawing = page.skeDrawings.get(drawingId); + if (skeDrawing) { + return { + skeDrawing, + pageMarginTop, + pageMarginLeft, + }; + } + + for (const table of page.skeTables.values()) { + for (const row of table.rows) { + for (const cell of row.cells) { + const cellAnchor = findDrawingAnchorInPage(cell, drawingId, cell.marginTop, cell.marginLeft); + if (cellAnchor) { + return cellAnchor; + } + } + } + } + + return null; +} + // eslint-disable-next-line max-lines-per-function function getDeleteAndInsertCustomBlockActions( segmentId: string, @@ -280,9 +316,7 @@ export const UpdateDocDrawingWrappingStyleCommand: ICommand = { } // Update positionH and positionV if layoutType is not inline. - let skeDrawing = null; - let pageMarginTop = 0; - let pageMarginLeft = 0; + let drawingAnchor: IDrawingAnchorInPage | null = null; for (const page of pages) { const { headerId, footerId, marginTop, marginLeft, marginBottom, pageWidth, pageHeight } = page; @@ -290,10 +324,8 @@ export const UpdateDocDrawingWrappingStyleCommand: ICommand = { case DocumentEditArea.HEADER: { const headerSke = skeHeaders.get(headerId)?.get(pageWidth); - if (headerSke != null && headerSke.skeDrawings.has(drawingId)) { - skeDrawing = headerSke.skeDrawings.get(drawingId); - pageMarginTop = headerSke.marginTop; - pageMarginLeft = marginLeft; + if (headerSke != null) { + drawingAnchor = findDrawingAnchorInPage(headerSke, drawingId, headerSke.marginTop, marginLeft); } break; @@ -301,30 +333,25 @@ export const UpdateDocDrawingWrappingStyleCommand: ICommand = { case DocumentEditArea.FOOTER: { const footerSke = skeFooters.get(footerId)?.get(pageWidth); - if (footerSke != null && footerSke.skeDrawings.has(drawingId)) { - skeDrawing = footerSke.skeDrawings.get(drawingId); - pageMarginTop = pageHeight - marginBottom + footerSke.marginTop; - pageMarginLeft = marginLeft; + if (footerSke != null) { + drawingAnchor = findDrawingAnchorInPage(footerSke, drawingId, pageHeight - marginBottom + footerSke.marginTop, marginLeft); } break; } case DocumentEditArea.BODY: { - if (page.skeDrawings.has(drawingId)) { - skeDrawing = page.skeDrawings.get(drawingId); - pageMarginTop = marginTop; - pageMarginLeft = marginLeft; - } + drawingAnchor = findDrawingAnchorInPage(page, drawingId, marginTop, marginLeft); break; } } - if (skeDrawing != null) { + if (drawingAnchor != null) { break; } } - if (skeDrawing != null) { + if (drawingAnchor != null) { + const { skeDrawing, pageMarginTop, pageMarginLeft } = drawingAnchor; const { aTop, aLeft } = skeDrawing; const oldPositionH = oldDrawings[drawingId].docTransform.positionH; let posOffsetH = aLeft; diff --git a/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts b/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts index 03e051a28a..685d99afa4 100644 --- a/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts +++ b/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts @@ -15,7 +15,7 @@ */ import type { IDocDrawingBase, IDocDrawingPosition, Nullable } from '@univerjs/core'; -import type { BaseObject, Documents, IDocumentSkeletonGlyph, IDocumentSkeletonPage, Image, INodeSearch, IPoint, Viewport } from '@univerjs/engine-render'; +import type { BaseObject, Documents, IDocumentSkeletonGlyph, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, Image, IPoint, Viewport } from '@univerjs/engine-render'; import type { IDrawingDocTransform } from '../commands/commands/update-doc-drawing.command'; import { BooleanNumber, @@ -36,6 +36,7 @@ import { DocSelectionRenderService, getAnchorBounding, getDocObject, getOneTextS import { IDrawingManagerService } from '@univerjs/drawing'; import { DocumentSkeletonPageType, getColor, IRenderManagerService, Liquid, PageLayoutType, Rect, Vector2 } from '@univerjs/engine-render'; import { IMoveInlineDrawingCommand, ITransformNonInlineDrawingCommand, UpdateDrawingDocTransformCommand } from '../commands/commands/update-doc-drawing.command'; +import { getDocsTableCellDrawingOffset } from './render-controllers/doc-drawing-transform-update.controller'; const INLINE_DRAWING_ANCHOR_KEY_PREFIX = '__InlineDrawingAnchor__'; @@ -56,10 +57,33 @@ interface IDrawingAnchor { contentBoxPointGroup?: IPoint[][]; } -function isInTableCell(nodePosition: INodeSearch) { - const { path } = nodePosition; +export interface IDocsTableCellAnchorContext { + cell: IDocumentSkeletonPage; + hostPage: IDocumentSkeletonPage; + offset: { + left: number; + top: number; + }; + row: IDocumentSkeletonRow; + table: IDocumentSkeletonTable; +} - return path.some((p) => p === 'cells'); +export function getDocsTableCellAnchorContext(unitId: string, cell: IDocumentSkeletonPage): Nullable { + const row = cell.parent as IDocumentSkeletonRow | undefined; + const table = row?.parent as IDocumentSkeletonTable | undefined; + const hostPage = table?.parent as IDocumentSkeletonPage | undefined; + + if (!row || !table || !hostPage || !row.cells?.includes(cell)) { + return null; + } + + return { + cell, + hostPage, + offset: getDocsTableCellDrawingOffset(unitId, table, row, cell), + row, + table, + }; } // Listen doc drawing transformer change, and update drawing data. @@ -434,11 +458,6 @@ export class DocDrawingTransformerController extends Disposable { return; } - // TODO: @JOCS, table cell do not support drawings now. so need to disable it. - if (isInTableCell(nodePosition)) { - return; - } - const positionWithIsBack = { ...nodePosition, isBack, @@ -534,7 +553,9 @@ export class DocDrawingTransformerController extends Disposable { this._liquid.reset(); - const pageType = page.type; + const tableCellContext = page.type === DocumentSkeletonPageType.CELL ? getDocsTableCellAnchorContext(drawing.unitId, page) : null; + const anchorPage = tableCellContext?.hostPage ?? page; + const pageType = anchorPage.type; for (const p of pages) { const { headerId, footerId, pageHeight, pageWidth, marginLeft, marginBottom } = p; @@ -571,13 +592,18 @@ export class DocDrawingTransformerController extends Disposable { break; } + + default: { + this._liquid.translatePagePadding(p); + break; + } } break; } this._liquid.translatePagePadding(p); - if (p === page) { + if (p === anchorPage) { break; } @@ -585,6 +611,10 @@ export class DocDrawingTransformerController extends Disposable { this._liquid.translatePage(p, pageLayoutType, pageMarginLeft, pageMarginTop); } + if (tableCellContext) { + this._liquid.translate(tableCellContext.offset.left, tableCellContext.offset.top); + } + if (positionV.relativeFrom === ObjectRelativeFromV.LINE) { glyphAnchor = line.divides[0].glyphGroup[0]; } else { @@ -637,11 +667,6 @@ export class DocDrawingTransformerController extends Disposable { return; } - // TODO: @JOCS, table cell do not support drawings now. so need to disable it. - if (isInTableCell(nodePosition)) { - return; - } - const positionWithIsBack = { ...nodePosition, isBack, diff --git a/packages/docs-drawing-ui/src/controllers/render-controllers/__tests__/doc-drawing-transform-update.controller.spec.ts b/packages/docs-drawing-ui/src/controllers/render-controllers/__tests__/doc-drawing-transform-update.controller.spec.ts new file mode 100644 index 0000000000..be8d7bfa54 --- /dev/null +++ b/packages/docs-drawing-ui/src/controllers/render-controllers/__tests__/doc-drawing-transform-update.controller.spec.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { setDocsTableRenderViewportProvider } from '@univerjs/engine-render'; +import { afterEach, describe, expect, it } from 'vitest'; +import { getDocsTableCellAnchorContext } from '../../doc-drawing-transformer-update.controller'; +import { getDocsTableCellDrawingOffset } from '../doc-drawing-transform-update.controller'; + +describe('DocDrawingTransformUpdateController', () => { + afterEach(() => { + setDocsTableRenderViewportProvider(null); + }); + + it('projects drawings in table cells through table, row, cell and scroll offsets', () => { + setDocsTableRenderViewportProvider((unitId, tableId) => { + if (unitId !== 'unit-1' || tableId !== 'table-1') { + return null; + } + + return { + contentWidth: 480, + scrollLeft: 30, + viewportWidth: 160, + }; + }); + + const table = { + left: 40, + tableId: 'table-1#-#0', + top: 80, + }; + const row = { + top: 12, + }; + const cell = { + left: 120, + marginLeft: 8, + marginTop: 6, + }; + + expect(getDocsTableCellDrawingOffset('unit-1', table as never, row as never, cell as never)).toEqual({ + left: 138, + top: 98, + }); + }); + + it('resolves a table cell drawing anchor to the host page and scrolled cell offset', () => { + setDocsTableRenderViewportProvider((unitId, tableId) => { + if (unitId !== 'unit-1' || tableId !== 'table-1') { + return null; + } + + return { + contentWidth: 480, + scrollLeft: 30, + viewportWidth: 160, + }; + }); + + const hostPage = { type: 'body' }; + const cell = { + left: 120, + marginLeft: 8, + marginTop: 6, + }; + const row = { + cells: [cell], + top: 12, + }; + const table = { + left: 40, + parent: hostPage, + rows: [row], + tableId: 'table-1#-#0', + top: 80, + }; + Object.assign(row, { parent: table }); + Object.assign(cell, { parent: row }); + + expect(getDocsTableCellAnchorContext('unit-1', cell as never)).toMatchObject({ + hostPage, + offset: { + left: 138, + top: 98, + }, + }); + }); +}); diff --git a/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-transform-update.controller.ts b/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-transform-update.controller.ts index f1e75dbae1..77dfa65cb2 100644 --- a/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-transform-update.controller.ts +++ b/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-transform-update.controller.ts @@ -16,7 +16,7 @@ import type { DocumentDataModel, ICommandInfo, IDrawingParam, ITransformState } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import type { Documents, DocumentSkeleton, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage, Image, IRenderContext, IRenderModule } from '@univerjs/engine-render'; +import type { Documents, DocumentSkeleton, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, Image, IRenderContext, IRenderModule } from '@univerjs/engine-render'; import { BooleanNumber, Disposable, @@ -32,7 +32,7 @@ import { import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { IEditorService, SetDocZoomRatioOperation } from '@univerjs/docs-ui'; import { IDrawingManagerService } from '@univerjs/drawing'; -import { Liquid, TRANSFORM_CHANGE_OBSERVABLE_TYPE } from '@univerjs/engine-render'; +import { getDocsTableRenderViewport, getTableIdAndSliceIndex, Liquid, TRANSFORM_CHANGE_OBSERVABLE_TYPE } from '@univerjs/engine-render'; import { debounceTime, filter } from 'rxjs'; import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service'; @@ -48,6 +48,23 @@ interface IDrawingParamsWithBehindText { isMultiTransform: BooleanNumber; } +export function getDocsTableCellDrawingOffset( + unitId: string, + table: IDocumentSkeletonTable, + row: IDocumentSkeletonRow, + cell: IDocumentSkeletonPage +) { + const sourceTableId = getTableIdAndSliceIndex(table.tableId).tableId; + const viewport = getDocsTableRenderViewport(unitId, sourceTableId); + const hasHorizontalViewport = viewport && viewport.contentWidth > viewport.viewportWidth; + const scrollLeft = hasHorizontalViewport ? viewport.scrollLeft : 0; + + return { + left: table.left + cell.left - scrollLeft + cell.marginLeft, + top: table.top + row.top + cell.marginTop, + }; +} + export class DocDrawingTransformUpdateController extends Disposable implements IRenderModule { private _liquid = new Liquid(); @@ -178,6 +195,15 @@ export class DocDrawingTransformUpdateController extends Disposable implements I headerPage.marginTop, page.marginLeft ); + this._calculateTableCellDrawingPositions( + unitId, + headerPage, + docsLeft, + docsTop, + updateDrawingMap, + headerPage.marginTop, + page.marginLeft + ); } } @@ -185,19 +211,30 @@ export class DocDrawingTransformUpdateController extends Disposable implements I const footerPage = skeFooters.get(footerId)?.get(pageWidth); if (footerPage) { + const footerTop = page.pageHeight - page.marginBottom + footerPage.marginTop; this._calculateDrawingPosition( unitId, footerPage, docsLeft, docsTop, updateDrawingMap, - page.pageHeight - page.marginBottom + footerPage.marginTop, + footerTop, + page.marginLeft + ); + this._calculateTableCellDrawingPositions( + unitId, + footerPage, + docsLeft, + docsTop, + updateDrawingMap, + footerTop, page.marginLeft ); } } this._calculateDrawingPosition(unitId, page, docsLeft, docsTop, updateDrawingMap, page.marginTop, page.marginLeft); + this._calculateTableCellDrawingPositions(unitId, page, docsLeft, docsTop, updateDrawingMap, page.marginTop, page.marginLeft); this._liquid.translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); } @@ -298,6 +335,45 @@ export class DocDrawingTransformUpdateController extends Disposable implements I } as IDocumentSkeletonPage); } + private _calculateTableCellDrawingPositions( + unitId: string, + page: IDocumentSkeletonPage | IDocumentSkeletonHeaderFooter, + docsLeft: number, + docsTop: number, + updateDrawingMap: Record, + baseMarginTop: number, + baseMarginLeft: number + ) { + page.skeTables?.forEach((table) => { + table.rows.forEach((row) => { + row.cells.forEach((cell) => { + const cellOffset = getDocsTableCellDrawingOffset(unitId, table, row, cell); + const marginTop = baseMarginTop + cellOffset.top; + const marginLeft = baseMarginLeft + cellOffset.left; + + this._calculateDrawingPosition( + unitId, + cell, + docsLeft, + docsTop, + updateDrawingMap, + marginTop, + marginLeft + ); + this._calculateTableCellDrawingPositions( + unitId, + cell, + docsLeft, + docsTop, + updateDrawingMap, + marginTop, + marginLeft + ); + }); + }); + }); + } + private _drawingInitializeListener() { const init = () => { const skeleton = this._docSkeletonManagerService.getSkeleton(); diff --git a/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-update.render-controller.ts b/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-update.render-controller.ts index 5b213b905d..0078651b26 100644 --- a/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-update.render-controller.ts +++ b/packages/docs-drawing-ui/src/controllers/render-controllers/doc-drawing-update.render-controller.ts @@ -36,6 +36,11 @@ import { SetDocDrawingArrangeCommand } from '../../commands/commands/set-drawing import { UngroupDocDrawingCommand } from '../../commands/commands/ungroup-doc-drawing.command'; import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service'; +interface IImageInsertPosition { + left: number; + top: number; +} + export class DocDrawingUpdateRenderController extends Disposable implements IRenderModule { constructor( private readonly _context: IRenderContext, @@ -68,6 +73,7 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen } async insertDocImage(): Promise { + const insertPosition = this._getCurrentImageInsertPosition(); const files = await this._fileOpenerService.openFile({ multiple: true, accept: DRAWING_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','), @@ -84,12 +90,12 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen return false; } - await this._insertFloatImages(files); + await this._insertFloatImages(files, insertPosition); return true; } // eslint-disable-next-line max-lines-per-function - private async _insertFloatImages(files: File[]) { + private async _insertFloatImages(files: File[], insertPosition: Nullable) { let imageParams: Nullable[] = []; try { @@ -141,12 +147,18 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen scale = Math.min(scaleWidth, scaleHeight); } - const docTransform = this._getImagePosition(width * scale, height * scale); + const imagePosition = insertPosition ?? this._getCurrentImageInsertPosition(); + const docTransform = this._getImagePosition(width * scale, height * scale, imagePosition); if (docTransform == null) { return; } + const transform = docDrawingPositionToTransform(docTransform); + if (transform != null && imagePosition != null) { + transform.top = imagePosition.top; + } + const docDrawingParam: IDocDrawing = { unitId, subUnitId: unitId, @@ -154,7 +166,7 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen drawingType: DrawingTypeEnum.DRAWING_IMAGE, imageSourceType, source, - transform: docDrawingPositionToTransform(docTransform), + transform, docTransform, behindDoc: BooleanNumber.FALSE, title: '', @@ -196,11 +208,11 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen private _getImagePosition( imageWidth: number, - imageHeight: number + imageHeight: number, + insertPosition?: Nullable ): Nullable { - const activeTextRange = this._docSelectionRenderService.getActiveTextRange(); // TODO: NO need to get the cursor position, because the insert image is inline. - const position = activeTextRange?.getAbsolutePosition() || { + const position = insertPosition ?? this._getCurrentImageInsertPosition() ?? { left: 0, top: 0, }; @@ -222,6 +234,19 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen }; } + private _getCurrentImageInsertPosition(): Nullable { + const position = this._docSelectionRenderService.getActiveTextRange()?.getAbsolutePosition(); + + if (position == null) { + return null; + } + + return { + left: position.left, + top: position.top, + }; + } + private _updateOrderListener() { this.disposeWithMe( this._drawingManagerService.featurePluginOrderUpdate$.subscribe((params) => { diff --git a/packages/docs-drawing-ui/src/index.ts b/packages/docs-drawing-ui/src/index.ts index de509e785c..489e6065b2 100644 --- a/packages/docs-drawing-ui/src/index.ts +++ b/packages/docs-drawing-ui/src/index.ts @@ -31,3 +31,5 @@ export { type IUniverDocsDrawingUIConfig } from './config/config'; export { DocFloatDomController } from './controllers/doc-float-dom.controller'; export { DOCS_IMAGE_MENU_ID } from './menu/image.menu'; export { UniverDocsDrawingUIPlugin } from './plugin'; +export { DocDrawingPosition } from './views/doc-image-panel/DocDrawingPosition'; +export { DocDrawingTextWrap } from './views/doc-image-panel/DocDrawingTextWrap'; diff --git a/packages/docs-drawing-ui/src/menu/__tests__/schema.spec.ts b/packages/docs-drawing-ui/src/menu/__tests__/schema.spec.ts new file mode 100644 index 0000000000..906e5bf322 --- /dev/null +++ b/packages/docs-drawing-ui/src/menu/__tests__/schema.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DOC_CONTENT_INSERT_MENU_ID, EMPTY_PARAGRAPH_MENU_ID, INSERT_BELLOW_MENU_ID } from '@univerjs/docs-ui'; +import { ContextMenuGroup, ContextMenuPosition } from '@univerjs/ui'; +import { describe, expect, it } from 'vitest'; +import { InsertDocImageCommand } from '../../commands/commands/insert-image.command'; +import { UploadFloatImageMenuFactory } from '../image.menu'; +import { menuSchema } from '../schema'; + +describe('docs drawing menu schema', () => { + it('adds image to paragraph insert menus', () => { + const paragraph = (menuSchema as any)[ContextMenuPosition.PARAGRAPH]; + + expect(paragraph[ContextMenuGroup.LAYOUT][INSERT_BELLOW_MENU_ID][InsertDocImageCommand.id].menuItemFactory).toBeDefined(); + expect(paragraph[EMPTY_PARAGRAPH_MENU_ID][ContextMenuGroup.LAYOUT][InsertDocImageCommand.id].menuItemFactory).toBeDefined(); + expect(paragraph[DOC_CONTENT_INSERT_MENU_ID][ContextMenuGroup.LAYOUT][InsertDocImageCommand.id].menuItemFactory).toBeDefined(); + }); + + it('uses the same image icon in paragraph insert menus', () => { + const item = UploadFloatImageMenuFactory({ get: () => undefined } as never); + + expect(item.icon).toBe('AddImageIcon'); + }); +}); diff --git a/packages/docs-drawing-ui/src/menu/drawing-popup-menu.controller.ts b/packages/docs-drawing-ui/src/menu/drawing-popup-menu.controller.ts index e2ec189311..18eaa0bebb 100644 --- a/packages/docs-drawing-ui/src/menu/drawing-popup-menu.controller.ts +++ b/packages/docs-drawing-ui/src/menu/drawing-popup-menu.controller.ts @@ -16,7 +16,7 @@ import type { DocumentDataModel, IDisposable, Nullable } from '@univerjs/core'; import type { BaseObject, Scene } from '@univerjs/engine-render'; -import { DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, IContextService, Inject, isInternalEditorID, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core'; +import { DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, ICommandService, IContextService, Inject, isInternalEditorID, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core'; import { DocCanvasPopManagerService } from '@univerjs/docs-ui'; import { IDrawingManagerService } from '@univerjs/drawing'; import { COMPONENT_IMAGE_POPUP_MENU, ImageCropperObject, ImageResetSizeOperation, OpenImageCropOperation } from '@univerjs/drawing-ui'; @@ -24,17 +24,20 @@ import { IRenderManagerService } from '@univerjs/engine-render'; import { takeUntil } from 'rxjs'; import { RemoveDocDrawingCommand } from '../commands/commands/remove-doc-drawing.command'; import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation'; +import { SidebarDocDrawingOperation } from '../commands/operations/open-drawing-panel.operation'; export class DocDrawingPopupMenuController extends RxDisposable { private _initImagePopupMenu = new Set(); private _disposePopups: IDisposable[] = []; + private _isDrawingPanelOpen = false; constructor( @IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService, @Inject(DocCanvasPopManagerService) private readonly _canvasPopManagerService: DocCanvasPopManagerService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @IContextService private readonly _contextService: IContextService + @IContextService private readonly _contextService: IContextService, + @ICommandService private readonly _commandService: ICommandService ) { super(); @@ -43,6 +46,29 @@ export class DocDrawingPopupMenuController extends RxDisposable { } private _init(): void { + this.disposeWithMe( + this._commandService.onCommandExecuted((command) => { + if (command.id === EditDocDrawingOperation.id) { + this._isDrawingPanelOpen = true; + this._clearPopups(); + } + if (command.id === SidebarDocDrawingOperation.id) { + const params = command.params as { value?: string } | undefined; + this._isDrawingPanelOpen = params?.value === 'open'; + if (this._isDrawingPanelOpen) { + this._clearPopups(); + } + } + }) + ); + this.disposeWithMe( + this._drawingManagerService.focus$.subscribe((params) => { + if (params.length === 0) { + this._isDrawingPanelOpen = false; + } + }) + ); + this.disposeWithMe( this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC).pipe(takeUntil(this.dispose$)).subscribe((documentDataModel) => this._create(documentDataModel)) ); @@ -56,13 +82,15 @@ export class DocDrawingPopupMenuController extends RxDisposable { private _dispose(documentDataModel: DocumentDataModel) { const unitId = documentDataModel.getUnitId(); - if (this._disposePopups.length) { - this._disposePopups.forEach((dispose) => dispose.dispose()); - this._disposePopups.length = 0; - } + this._clearPopups(); this._renderManagerService.removeRender(unitId); } + private _clearPopups() { + this._disposePopups.forEach((dispose) => dispose.dispose()); + this._disposePopups.length = 0; + } + private _create(documentDataModel: Nullable) { if (!documentDataModel) { return; @@ -112,6 +140,9 @@ export class DocDrawingPopupMenuController extends RxDisposable { const selectedObjects = transformer.getSelectedObjectMap(); disposePopups.forEach((dispose) => dispose.dispose()); disposePopups.length = 0; + if (this._isDrawingPanelOpen) { + return; + } if (selectedObjects.size > 1) { return; } @@ -123,19 +154,28 @@ export class DocDrawingPopupMenuController extends RxDisposable { const oKey = object.oKey; const drawingParam = this._drawingManagerService.getDrawingOKey(oKey); - if (!drawingParam || drawingParam.drawingType === DrawingTypeEnum.DRAWING_DOM) { + if ( + !drawingParam || + drawingParam.drawingType === DrawingTypeEnum.DRAWING_DOM || + drawingParam.drawingType === DrawingTypeEnum.DRAWING_SHAPE + ) { return; } const { unitId, subUnitId, drawingId, drawingType } = drawingParam; + const isImage = drawingType === DrawingTypeEnum.DRAWING_IMAGE; const popup = this._canvasPopManagerService.attachPopupToObject( object, { componentKey: COMPONENT_IMAGE_POPUP_MENU, - direction: 'horizontal', - offset: [2, 0], + direction: isImage ? 'top-center' : 'horizontal', + offset: isImage ? [0, 8] : [2, 0], extraProps: { menuItems: this._getImageMenuItems(unitId, subUnitId, drawingId, drawingType), + variant: isImage ? 'doc-floating-toolbar' : undefined, + unitId, + subUnitId, + drawingId, }, }, unitId @@ -191,8 +231,7 @@ export class DocDrawingPopupMenuController extends RxDisposable { index: 0, commandId: EditDocDrawingOperation.id, commandParams: { unitId, subUnitId, drawingId }, - // disable: !!SHEET_EDITOR_UNITS.includes(unitId) || drawingType === DrawingTypeEnum.DRAWING_DOM, - disable: true, + disable: drawingType === DrawingTypeEnum.DRAWING_DOM, }, { label: 'docs-drawing-ui.image-popup.delete', @@ -206,7 +245,7 @@ export class DocDrawingPopupMenuController extends RxDisposable { index: 2, commandId: OpenImageCropOperation.id, commandParams: { unitId, subUnitId, drawingId }, - disable: true, // TODO: @JOCS, feature is not ready. + disable: drawingType === DrawingTypeEnum.DRAWING_DOM, }, { label: 'docs-drawing-ui.image-popup.reset', diff --git a/packages/docs-drawing-ui/src/menu/image.menu.ts b/packages/docs-drawing-ui/src/menu/image.menu.ts index 7fb04c4947..f82ed8f8a4 100644 --- a/packages/docs-drawing-ui/src/menu/image.menu.ts +++ b/packages/docs-drawing-ui/src/menu/image.menu.ts @@ -77,6 +77,7 @@ export function UploadFloatImageMenuFactory(_accessor: IAccessor): IMenuItem { id: IMAGE_MENU_UPLOAD_FLOAT_ID, title: 'docs-drawing-ui.upload.float', type: MenuItemType.BUTTON, + icon: 'AddImageIcon', hidden$: getMenuHiddenObservable(_accessor, UniverInstanceType.UNIVER_DOC, undefined, DOCS_ZEN_EDITOR_UNIT_ID_KEY), }; } diff --git a/packages/docs-drawing-ui/src/menu/schema.ts b/packages/docs-drawing-ui/src/menu/schema.ts index 1db0639ac6..9b2a96fee8 100644 --- a/packages/docs-drawing-ui/src/menu/schema.ts +++ b/packages/docs-drawing-ui/src/menu/schema.ts @@ -15,7 +15,8 @@ */ import type { MenuSchemaType } from '@univerjs/ui'; -import { RibbonInsertGroup } from '@univerjs/ui'; +import { DOC_CONTENT_INSERT_MENU_ID, EMPTY_PARAGRAPH_MENU_ID, INSERT_BELLOW_MENU_ID } from '@univerjs/docs-ui'; +import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui'; import { DOCS_IMAGE_MENU_ID, IMAGE_MENU_UPLOAD_FLOAT_ID, @@ -34,4 +35,30 @@ export const menuSchema: MenuSchemaType = { }, }, }, + [ContextMenuPosition.PARAGRAPH]: { + [ContextMenuGroup.LAYOUT]: { + [INSERT_BELLOW_MENU_ID]: { + [IMAGE_MENU_UPLOAD_FLOAT_ID]: { + order: 5, + menuItemFactory: UploadFloatImageMenuFactory, + }, + }, + }, + [EMPTY_PARAGRAPH_MENU_ID]: { + [ContextMenuGroup.LAYOUT]: { + [IMAGE_MENU_UPLOAD_FLOAT_ID]: { + order: 5, + menuItemFactory: UploadFloatImageMenuFactory, + }, + }, + }, + [DOC_CONTENT_INSERT_MENU_ID]: { + [ContextMenuGroup.LAYOUT]: { + [IMAGE_MENU_UPLOAD_FLOAT_ID]: { + order: 5, + menuItemFactory: UploadFloatImageMenuFactory, + }, + }, + }, + }, }; diff --git a/packages/docs-drawing-ui/src/views/doc-image-panel/DocDrawingPanel.tsx b/packages/docs-drawing-ui/src/views/doc-image-panel/DocDrawingPanel.tsx index 1286126eef..c25126cfd9 100644 --- a/packages/docs-drawing-ui/src/views/doc-image-panel/DocDrawingPanel.tsx +++ b/packages/docs-drawing-ui/src/views/doc-image-panel/DocDrawingPanel.tsx @@ -40,7 +40,7 @@ export const DocDrawingPanel = () => { return !!drawings?.length && (

- +
diff --git a/packages/docs-hyper-link-ui/src/controllers/render-controllers/__tests__/hyper-link-event.render-controller.spec.ts b/packages/docs-hyper-link-ui/src/controllers/render-controllers/__tests__/hyper-link-event.render-controller.spec.ts new file mode 100644 index 0000000000..3391d55876 --- /dev/null +++ b/packages/docs-hyper-link-ui/src/controllers/render-controllers/__tests__/hyper-link-event.render-controller.spec.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { config, Subject } from 'rxjs'; +import { describe, expect, it, vi } from 'vitest'; +import { DocHyperLinkEventRenderController } from '../hyper-link-event.render-controller'; + +describe('DocHyperLinkEventRenderController', () => { + it('ignores hover ranges when the current selection has no text ranges', async () => { + const hoverCustomRanges$ = new Subject(); + const clickCustomRanges$ = new Subject(); + const onUnhandledError = vi.fn(); + const previousUnhandledError = config.onUnhandledError; + const commandService = { + executeCommand: vi.fn(), + }; + config.onUnhandledError = onUnhandledError; + + try { + const controller = new DocHyperLinkEventRenderController( + { unitId: 'doc-unit' } as never, + { hoverCustomRanges$, clickCustomRanges$ } as never, + commandService as never, + { showing: false } as never, + { getSkeleton: vi.fn() } as never, + { getTextRanges: () => [] } as never + ); + + hoverCustomRanges$.next([]); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(onUnhandledError).not.toHaveBeenCalled(); + expect(commandService.executeCommand).not.toHaveBeenCalled(); + + controller.dispose(); + } finally { + config.onUnhandledError = previousUnhandledError; + } + }); +}); diff --git a/packages/docs-hyper-link-ui/src/controllers/render-controllers/hyper-link-event.render-controller.ts b/packages/docs-hyper-link-ui/src/controllers/render-controllers/hyper-link-event.render-controller.ts index 03ce4dcea2..1c5730f81a 100644 --- a/packages/docs-hyper-link-ui/src/controllers/render-controllers/hyper-link-event.render-controller.ts +++ b/packages/docs-hyper-link-ui/src/controllers/render-controllers/hyper-link-event.render-controller.ts @@ -58,7 +58,7 @@ export class DocHyperLinkEventRenderController extends Disposable implements IRe this._docEventManagerService.hoverCustomRanges$.subscribe((ranges) => { const link = ranges.find((range) => range.range.rangeType === CustomRangeType.HYPERLINK); const activeRanges = this._docSelectionManagerService.getTextRanges(); - const currentSegmentId = activeRanges?.[0].segmentId; + const currentSegmentId = activeRanges?.[0]?.segmentId; if ((link?.segmentId ?? '') !== currentSegmentId) { this._hideInfoPopup(); return; diff --git a/packages/docs-hyper-link-ui/src/menu/__tests__/schema.spec.ts b/packages/docs-hyper-link-ui/src/menu/__tests__/schema.spec.ts new file mode 100644 index 0000000000..c24c5b54fd --- /dev/null +++ b/packages/docs-hyper-link-ui/src/menu/__tests__/schema.spec.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EMPTY_PARAGRAPH_MENU_ID, INSERT_BELLOW_MENU_ID } from '@univerjs/docs-ui'; +import { ContextMenuGroup, ContextMenuPosition } from '@univerjs/ui'; +import { describe, expect, it } from 'vitest'; +import { ShowDocHyperLinkEditPopupOperation } from '../../commands/operations/popup.operation'; +import { menuSchema } from '../schema'; + +describe('docs hyperlink menu schema', () => { + it('adds hyperlink to paragraph insert menus', () => { + const paragraph = (menuSchema as any)[ContextMenuPosition.PARAGRAPH]; + + expect(paragraph[ContextMenuGroup.LAYOUT][INSERT_BELLOW_MENU_ID][ShowDocHyperLinkEditPopupOperation.id].menuItemFactory).toBeDefined(); + expect(paragraph[EMPTY_PARAGRAPH_MENU_ID][ContextMenuGroup.LAYOUT][ShowDocHyperLinkEditPopupOperation.id].menuItemFactory).toBeDefined(); + }); +}); diff --git a/packages/docs-hyper-link-ui/src/menu/schema.ts b/packages/docs-hyper-link-ui/src/menu/schema.ts index 3091de80ab..dc3656ae3e 100644 --- a/packages/docs-hyper-link-ui/src/menu/schema.ts +++ b/packages/docs-hyper-link-ui/src/menu/schema.ts @@ -15,6 +15,7 @@ */ import type { MenuSchemaType } from '@univerjs/ui'; +import { EMPTY_PARAGRAPH_MENU_ID, INSERT_BELLOW_MENU_ID } from '@univerjs/docs-ui'; import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui'; import { ShowDocHyperLinkEditPopupOperation } from '../commands/operations/popup.operation'; import { AddHyperLinkMenuItemFactory } from './menu'; @@ -34,4 +35,22 @@ export const menuSchema: MenuSchemaType = { }, }, }, + [ContextMenuPosition.PARAGRAPH]: { + [ContextMenuGroup.LAYOUT]: { + [INSERT_BELLOW_MENU_ID]: { + [ShowDocHyperLinkEditPopupOperation.id]: { + order: 6, + menuItemFactory: AddHyperLinkMenuItemFactory, + }, + }, + }, + [EMPTY_PARAGRAPH_MENU_ID]: { + [ContextMenuGroup.LAYOUT]: { + [ShowDocHyperLinkEditPopupOperation.id]: { + order: 6, + menuItemFactory: AddHyperLinkMenuItemFactory, + }, + }, + }, + }, }; diff --git a/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts b/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts index d6c4ceaf95..ee5bcbd12d 100644 --- a/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts +++ b/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts @@ -54,7 +54,7 @@ const noopDisposable = { dispose: () => {}, }; -interface IKeywordInputPlaceholderExtraProps { +interface IKeywordInputPlaceholderExtraProps extends Record { fontSize?: number; fontString?: string; fontFamily?: string; diff --git a/packages/docs-ui/src/commands/commands/__tests__/core-editing.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/core-editing.command.spec.ts index 4b13236343..56c07dd2c2 100644 --- a/packages/docs-ui/src/commands/commands/__tests__/core-editing.command.spec.ts +++ b/packages/docs-ui/src/commands/commands/__tests__/core-editing.command.spec.ts @@ -19,15 +19,18 @@ import { awaitTime, BooleanNumber, CustomRangeType, + DataStreamTreeTokenType, + HorizontalAlign, ICommandService, IUniverInstanceService, UniverInstanceType, UpdateDocsAttributeType, } from '@univerjs/core'; -import { DocSelectionManagerService, RichTextEditingMutation, SetTextSelectionsOperation } from '@univerjs/docs'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { DocSelectionManagerService, DocSkeletonManagerService, RichTextEditingMutation, SetTextSelectionsOperation } from '@univerjs/docs'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DeleteDirection } from '../../../types/delete-direction'; import { DeleteCommand, InsertCommand, UpdateCommand } from '../core-editing.command'; +import { DeleteLeftCommand, DeleteRightCommand, isDeleteOffsetInsideBlockRange } from '../doc-delete.command'; import { createCommandTestBed } from './create-command-test-bed'; function getDocumentData(): IDocumentData { @@ -63,6 +66,108 @@ function getDocumentData(): IDocumentData { }; } +function getCenteredSingleCharacterDocumentData(): IDocumentData { + return { + id: 'test-doc', + body: { + dataStream: 'A\r\n', + textRuns: [{ + st: 0, + ed: 1, + ts: {}, + }], + paragraphs: [{ + startIndex: 1, + paragraphStyle: { + horizontalAlign: HorizontalAlign.CENTER, + }, + }], + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginRight: 90, + marginLeft: 90, + }, + }; +} + +function getCenteredEmptyParagraphDocumentData(): IDocumentData { + return { + id: 'test-doc', + body: { + dataStream: '\r\n', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + horizontalAlign: HorizontalAlign.CENTER, + }, + }], + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginRight: 90, + marginLeft: 90, + }, + }; +} + +function getIndentedBlockRangeDocumentData(): IDocumentData { + return { + id: 'test-doc', + body: { + dataStream: `${DataStreamTreeTokenType.BLOCK_START}A${DataStreamTreeTokenType.PARAGRAPH}${DataStreamTreeTokenType.BLOCK_END}\n`, + paragraphs: [{ + startIndex: 2, + paragraphStyle: { + indentStart: { v: 22 }, + }, + }], + blockRanges: [{ + blockId: 'quote-1', + blockType: 'quote', + startIndex: 0, + endIndex: 3, + }], + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginRight: 90, + marginLeft: 90, + }, + }; +} + +function createFirstGlyph(paragraphIndex: number, content: string) { + const line = { paragraphIndex, divides: [] as any[] }; + const divide = { parent: line, glyphGroup: [] as any[] }; + const glyph = { + parent: divide, + content, + streamType: content, + count: 1, + }; + + divide.glyphGroup.push(glyph); + line.divides.push(divide); + + return glyph; +} + describe('core editing commands', () => { let univer: Univer; let get: Injector['get']; @@ -87,6 +192,28 @@ describe('core editing commands', () => { } } + function setActiveSelection(offset: number) { + const selectionManager = get(DocSelectionManagerService); + selectionManager.__TEST_ONLY_setCurrentSelection({ + unitId: 'test-doc', + subUnitId: 'test-doc', + }); + selectionManager.__TEST_ONLY_add([{ startOffset: offset, endOffset: offset, collapsed: true, isActive: true, segmentId: '', style: null as never }]); + } + + function mockSkeleton() { + const skeletonManager = get(DocSkeletonManagerService) as unknown as { getSkeleton: () => unknown }; + skeletonManager.getSkeleton = () => ({}); + } + + function registerDeleteKeyCommands() { + commandService.registerCommand(DeleteLeftCommand); + commandService.registerCommand(DeleteRightCommand); + commandService.registerCommand(UpdateCommand); + commandService.registerCommand(SetTextSelectionsOperation); + commandService.registerCommand(RichTextEditingMutation as unknown as ICommand); + } + beforeEach(() => { const testBed = createCommandTestBed(getDocumentData()); univer = testBed.univer; @@ -140,6 +267,156 @@ describe('core editing commands', () => { expect(getBody()?.customRanges).toEqual([]); }); + it('keeps center alignment when deleting the last character from a centered paragraph', async () => { + univer.dispose(); + const testBed = createCommandTestBed(getCenteredSingleCharacterDocumentData()); + univer = testBed.univer; + get = testBed.get; + commandService = get(ICommandService); + commandService.registerCommand(DeleteCommand); + commandService.registerCommand(SetTextSelectionsOperation); + commandService.registerCommand(RichTextEditingMutation as unknown as ICommand); + + await commandService.executeCommand(DeleteCommand.id, { + unitId: 'test-doc', + segmentId: '', + range: { startOffset: 0, endOffset: 0, collapsed: true }, + direction: DeleteDirection.RIGHT, + }); + + await awaitTime(0); + + expect(getDataStream()).toBe('\r\n'); + expect(getBody()?.paragraphs?.[0].paragraphStyle?.horizontalAlign).toBe(HorizontalAlign.CENTER); + }); + + it('keeps center alignment when backspacing the last character from a centered paragraph', async () => { + univer.dispose(); + const testBed = createCommandTestBed(getCenteredSingleCharacterDocumentData()); + univer = testBed.univer; + get = testBed.get; + commandService = get(ICommandService); + commandService.registerCommand(DeleteCommand); + commandService.registerCommand(SetTextSelectionsOperation); + commandService.registerCommand(RichTextEditingMutation as unknown as ICommand); + + await commandService.executeCommand(DeleteCommand.id, { + unitId: 'test-doc', + segmentId: '', + range: { startOffset: 1, endOffset: 1, collapsed: true }, + direction: DeleteDirection.LEFT, + }); + + await awaitTime(0); + + expect(getDataStream()).toBe('\r\n'); + expect(getBody()?.paragraphs?.[0].paragraphStyle?.horizontalAlign).toBe(HorizontalAlign.CENTER); + }); + + it('resets an empty centered paragraph to left alignment on a second delete', async () => { + univer.dispose(); + const testBed = createCommandTestBed(getCenteredEmptyParagraphDocumentData()); + univer = testBed.univer; + get = testBed.get; + commandService = get(ICommandService); + registerDeleteKeyCommands(); + mockSkeleton(); + setActiveSelection(0); + + await commandService.executeCommand(DeleteRightCommand.id); + + await awaitTime(0); + + expect(getDataStream()).toBe('\r\n'); + expect(getBody()?.paragraphs?.[0].paragraphStyle?.horizontalAlign).toBe(HorizontalAlign.LEFT); + }); + + it('resets an empty centered paragraph to left alignment on a second backspace', async () => { + univer.dispose(); + const testBed = createCommandTestBed(getCenteredEmptyParagraphDocumentData()); + univer = testBed.univer; + get = testBed.get; + commandService = get(ICommandService); + registerDeleteKeyCommands(); + mockSkeleton(); + setActiveSelection(0); + + await commandService.executeCommand(DeleteLeftCommand.id); + + await awaitTime(0); + + expect(getDataStream()).toBe('\r\n'); + expect(getBody()?.paragraphs?.[0].paragraphStyle?.horizontalAlign).toBe(HorizontalAlign.LEFT); + }); + + it('detects delete offsets inside block ranges so backspace does not clear block paragraph indent', () => { + const dataStream = `${DataStreamTreeTokenType.BLOCK_START}A${DataStreamTreeTokenType.PARAGRAPH}${DataStreamTreeTokenType.BLOCK_END}\n`; + const body = { + dataStream, + blockRanges: [{ + blockId: 'block-1', + blockType: 'quote', + startIndex: 0, + endIndex: 3, + }], + }; + + expect(isDeleteOffsetInsideBlockRange(body, 1)).toBe(true); + expect(isDeleteOffsetInsideBlockRange(body, 2)).toBe(true); + expect(isDeleteOffsetInsideBlockRange(body, 0)).toBe(false); + expect(isDeleteOffsetInsideBlockRange(body, 3)).toBe(false); + }); + + it('detects block ranges whose endIndex points before the block end token', () => { + const dataStream = `${DataStreamTreeTokenType.BLOCK_START}A${DataStreamTreeTokenType.PARAGRAPH}${DataStreamTreeTokenType.BLOCK_END}\n`; + const body = { + dataStream, + blockRanges: [{ + blockId: 'block-1', + blockType: 'callout', + startIndex: 0, + endIndex: 2, + }], + }; + + expect(isDeleteOffsetInsideBlockRange(body, 1)).toBe(true); + expect(isDeleteOffsetInsideBlockRange(body, 2)).toBe(true); + expect(isDeleteOffsetInsideBlockRange(body, 3)).toBe(false); + }); + + it('does not clear paragraph indent when backspacing at the start of a block range paragraph', async () => { + univer.dispose(); + const testBed = createCommandTestBed(getIndentedBlockRangeDocumentData()); + univer = testBed.univer; + get = testBed.get; + commandService = get(ICommandService); + registerDeleteKeyCommands(); + setActiveSelection(1); + + const curGlyph = createFirstGlyph(2, 'A'); + const preGlyph = createFirstGlyph(2, DataStreamTreeTokenType.BLOCK_START); + const skeletonManager = get(DocSkeletonManagerService) as unknown as { getSkeleton: () => unknown }; + skeletonManager.getSkeleton = () => ({ + findNodeByCharIndex: (offset: number) => offset === 1 ? curGlyph : preGlyph, + }); + + const originalExecuteCommand = commandService.executeCommand.bind(commandService); + const executeSpy = vi.spyOn(commandService, 'executeCommand').mockImplementation(async (id: string, params?: object) => { + if (id === DeleteLeftCommand.id) { + return originalExecuteCommand(id, params); + } + + return true; + }); + + await commandService.executeCommand(DeleteLeftCommand.id); + + expect(executeSpy).not.toHaveBeenCalledWith(UpdateCommand.id, expect.anything()); + expect(executeSpy).toHaveBeenCalledWith(DeleteCommand.id, expect.objectContaining({ + direction: DeleteDirection.LEFT, + })); + }); + it('updates text styles through the shared rich text mutation flow', async () => { await commandService.executeCommand(UpdateCommand.id, { unitId: 'test-doc', diff --git a/packages/docs-ui/src/commands/commands/__tests__/doc-block-move.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/doc-block-move.command.spec.ts new file mode 100644 index 0000000000..a164d21e03 --- /dev/null +++ b/packages/docs-ui/src/commands/commands/__tests__/doc-block-move.command.spec.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDocumentData } from '@univerjs/core'; +import { BlockType, CustomRangeType } from '@univerjs/core'; +import { describe, expect, it } from 'vitest'; +import { buildMoveDocBlockActions } from '../doc-block-move.command'; + +describe('buildMoveDocBlockActions', () => { + it('moves a paragraph and remaps paragraph indexes', () => { + const documentData = createDocument('A\rB\rC\r\n', { + paragraphs: [{ startIndex: 1 }, { startIndex: 3 }, { startIndex: 5 }], + sectionBreaks: [{ startIndex: 6 }], + }); + + const { nextDocumentData, movedRange } = buildMoveDocBlockActions({ + documentData, + sourceRange: { startOffset: 0, endOffset: 2 }, + targetOffset: 6, + }); + + expect(nextDocumentData.body?.dataStream).toBe('B\rC\rA\r\n'); + expect(nextDocumentData.body?.paragraphs?.map((item) => item.startIndex)).toEqual([1, 3, 5]); + expect(movedRange).toEqual({ startOffset: 4, endOffset: 6 }); + }); + + it('moves a block range as one unit', () => { + const documentData = createDocument('aa\rBB\rcc\r\n', { + paragraphs: [{ startIndex: 2 }, { startIndex: 5 }, { startIndex: 8 }], + sectionBreaks: [{ startIndex: 9 }], + blockRanges: [{ blockId: 'quote-1', blockType: 'quote', startIndex: 3, endIndex: 5 }], + }); + + const { nextDocumentData, movedRange } = buildMoveDocBlockActions({ + documentData, + sourceRange: { startOffset: 3, endOffset: 6 }, + targetOffset: 0, + }); + + expect(nextDocumentData.body?.dataStream).toBe('BB\raa\rcc\r\n'); + expect(nextDocumentData.body?.blockRanges?.[0]).toMatchObject({ startIndex: 0, endIndex: 2 }); + expect(nextDocumentData.body?.paragraphs?.map((item) => item.startIndex)).toEqual([2, 5, 8]); + expect(movedRange).toEqual({ startOffset: 0, endOffset: 3 }); + }); + + it('moves a table range and remaps custom ranges and text runs', () => { + const documentData = createDocument('aa\rTT\rcc\r\n', { + paragraphs: [{ startIndex: 2 }, { startIndex: 5 }, { startIndex: 8 }], + sectionBreaks: [{ startIndex: 9 }], + tables: [{ tableId: 'table-1', startIndex: 3, endIndex: 6 }], + customRanges: [{ rangeId: 'comment-1', rangeType: CustomRangeType.COMMENT, startIndex: 6, endIndex: 7 }], + textRuns: [{ st: 6, ed: 8, ts: {} }], + }); + + const { nextDocumentData } = buildMoveDocBlockActions({ + documentData, + sourceRange: { startOffset: 3, endOffset: 6 }, + targetOffset: 0, + }); + + expect(nextDocumentData.body?.dataStream).toBe('TT\raa\rcc\r\n'); + expect(nextDocumentData.body?.tables?.[0]).toMatchObject({ startIndex: 0, endIndex: 3 }); + expect(nextDocumentData.body?.customRanges?.[0]).toMatchObject({ startIndex: 6, endIndex: 7 }); + expect(nextDocumentData.body?.textRuns?.[0]).toMatchObject({ st: 6, ed: 8 }); + }); + + it('moves a custom block paragraph and keeps the custom block attached', () => { + const documentData = createDocument('\b\raa\r\n', { + paragraphs: [{ startIndex: 1 }, { startIndex: 4 }], + sectionBreaks: [{ startIndex: 5 }], + customBlocks: [{ blockId: 'custom-1', blockType: BlockType.CUSTOM, startIndex: 0 }], + }); + + const { nextDocumentData } = buildMoveDocBlockActions({ + documentData, + sourceRange: { startOffset: 0, endOffset: 2 }, + targetOffset: 5, + }); + + expect(nextDocumentData.body?.dataStream).toBe('aa\r\b\r\n'); + expect(nextDocumentData.body?.customBlocks?.[0]).toMatchObject({ startIndex: 3 }); + }); +}); + +function createDocument(dataStream: string, body: Partial>): IDocumentData { + return { + id: 'test-doc', + body: { + dataStream, + customBlocks: [], + ...body, + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginLeft: 72, + marginRight: 72, + }, + }; +} diff --git a/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts index d9db328ee6..b11079d6c9 100644 --- a/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts +++ b/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts @@ -15,7 +15,7 @@ */ import type { DocumentDataModel, ICommand, Injector, IStyleBase, Univer } from '@univerjs/core'; -import { BooleanNumber, ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core'; +import { BooleanNumber, DOC_RANGE_TYPE, ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation, SetTextSelectionsOperation } from '@univerjs/docs'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { @@ -116,6 +116,51 @@ describe('Test inline format commands', () => { expect(getFormatValueAt('bl', 21)).toBe(BooleanNumber.TRUE); expect(getFormatValueAt('bl', 25)).toBe(BooleanNumber.TRUE); }); + + it('formats text ranges without applying stale table rect ranges', async () => { + const selectionManager = get(DocSelectionManagerService); + selectionManager.__replaceTextRangesWithNoRefresh({ + textRanges: [{ + startOffset: 0, + endOffset: 5, + collapsed: false, + isActive: true, + rangeType: DOC_RANGE_TYPE.TEXT, + }], + rectRanges: [{ + startOffset: 20, + endOffset: 30, + collapsed: false, + rangeType: DOC_RANGE_TYPE.RECT, + tableId: 'table-1', + startRow: 0, + endRow: 0, + startColumn: 0, + endColumn: 0, + }], + segmentId: '', + segmentPage: -1, + isEditing: true, + style: {}, + } as never, { + unitId: 'test-doc', + subUnitId: 'test-doc', + }); + + expect(getFormatValueAt('ff', 1)).toBe(undefined); + expect(getFormatValueAt('ff', 21)).toBe(undefined); + + await commandService.executeCommand(SetInlineFormatCommand.id, { + segmentId: '', + preCommandId: SetInlineFormatFontFamilyCommand.id, + value: 'Arial', + }); + + expect(getFormatValueAt('ff', 1)).toBe('Arial'); + expect(getFormatValueAt('ff', 21)).toBe(undefined); + + await commandService.executeCommand(UndoCommand.id); + }); }); describe('Set Italic by SetInlineFormatCommand', () => { diff --git a/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts index d987aac803..c103503482 100644 --- a/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts +++ b/packages/docs-ui/src/commands/commands/__tests__/misc.command.spec.ts @@ -163,6 +163,61 @@ function createTableDoc(): IDocumentData { }; } +function createTableDocWithParagraphsBeforeTable(): IDocumentData { + const table = genEmptyTable(2, 2); + const prefix = 'Title\rBody\r'; + const suffix = 'Tail\r\n'; + const dataStream = `${prefix}${table.dataStream}${suffix}`; + + return { + id: 'test-doc', + body: { + dataStream, + textRuns: [{ + st: 0, + ed: dataStream.length - 2, + ts: {}, + }], + paragraphs: [ + { startIndex: 5 }, + { startIndex: 10 }, + ...table.paragraphs.map((paragraph) => ({ + ...paragraph, + startIndex: paragraph.startIndex + prefix.length, + })), + { + startIndex: dataStream.length - 2, + }, + ], + sectionBreaks: [ + ...table.sectionBreaks.map((sectionBreak) => ({ + ...sectionBreak, + startIndex: sectionBreak.startIndex + prefix.length, + })), + { + startIndex: dataStream.length - 1, + }, + ], + tables: [{ + startIndex: prefix.length, + endIndex: prefix.length + table.dataStream.length, + tableId: 'table-1', + }], + customBlocks: [], + }, + documentStyle: { + pageSize: { + width: 594.3, + height: 840.51, + }, + marginTop: 72, + marginBottom: 72, + marginRight: 90, + marginLeft: 90, + }, + }; +} + describe('misc document commands', () => { let univer: Univer; let get: Injector['get']; @@ -290,7 +345,7 @@ describe('misc document commands', () => { subscription.unsubscribe(); }); - it('selects text and table ranges when tables are present', async () => { + it('selects the current paragraph first when tables are present', async () => { ({ univer, get } = createCommandTestBed(createTableDoc())); commandService = get(ICommandService); commandService.registerCommand(DocSelectAllCommand); @@ -317,16 +372,136 @@ describe('misc document commands', () => { startOffset: 0, endOffset: 1, }), + ], + })); + + subscription.unsubscribe(); + }); + + it('expands to the whole body when the current paragraph selection is split into visual ranges', async () => { + ({ univer, get } = createCommandTestBed(createTableDoc())); + commandService = get(ICommandService); + commandService.registerCommand(DocSelectAllCommand); + + const selectionManager = get(DocSelectionManagerService); + selectionManager.__TEST_ONLY_setCurrentSelection({ + unitId: 'test-doc', + subUnitId: 'test-doc', + }); + selectionManager.__TEST_ONLY_add([{ + startOffset: 0, + endOffset: 0, + collapsed: false, + isActive: true, + segmentId: '', + style: null as never, + }, { + startOffset: 1, + endOffset: 1, + collapsed: false, + isActive: false, + segmentId: '', + style: null as never, + }], false); + + const refreshEvents: Array = []; + const subscription = selectionManager.refreshSelection$.subscribe((event) => { + if (event) { + refreshEvents.push(event); + } + }); + + const result = await commandService.executeCommand(DocSelectAllCommand.id); + await awaitTime(0); + + expect(result).toBe(true); + expect(refreshEvents.at(-1)).toEqual(expect.objectContaining({ + unitId: 'test-doc', + subUnitId: 'test-doc', + isEditing: false, + docRanges: [ + expect.objectContaining({ startOffset: 0, endOffset: 1 }), + expect.objectContaining({ startOffset: 5, endOffset: 19, rangeType: 'RECT' }), + expect.objectContaining({ startOffset: 24, endOffset: 26 }), + ], + })); + + subscription.unsubscribe(); + }); + + it('expands to the whole body by keeping text before tables selectable across paragraphs', async () => { + ({ univer, get } = createCommandTestBed(createTableDocWithParagraphsBeforeTable())); + commandService = get(ICommandService); + commandService.registerCommand(DocSelectAllCommand); + + const selectionManager = get(DocSelectionManagerService); + selectionManager.__TEST_ONLY_setCurrentSelection({ + unitId: 'test-doc', + subUnitId: 'test-doc', + }); + selectionManager.__TEST_ONLY_add([{ + startOffset: 0, + endOffset: 5, + collapsed: false, + isActive: true, + segmentId: '', + style: null as never, + }], false); + + const refreshEvents: Array = []; + const subscription = selectionManager.refreshSelection$.subscribe((event) => { + if (event) { + refreshEvents.push(event); + } + }); + + const result = await commandService.executeCommand(DocSelectAllCommand.id); + await awaitTime(0); + + expect(result).toBe(true); + expect(refreshEvents.at(-1)).toEqual(expect.objectContaining({ + unitId: 'test-doc', + subUnitId: 'test-doc', + isEditing: false, + docRanges: [ + expect.objectContaining({ startOffset: 0, endOffset: 5 }), + expect.objectContaining({ startOffset: 6, endOffset: 10 }), + expect.objectContaining({ rangeType: 'RECT' }), + expect.objectContaining({ startOffset: expect.any(Number), endOffset: expect.any(Number), rangeType: 'TEXT' }), + ], + })); + + subscription.unsubscribe(); + }); + + it('selects the current table first when the cursor is inside a table', async () => { + ({ univer, get } = createCommandTestBed(createTableDoc())); + commandService = get(ICommandService); + commandService.registerCommand(DocSelectAllCommand); + setCollapsedSelection(6); + + const selectionManager = get(DocSelectionManagerService); + const refreshEvents: Array = []; + const subscription = selectionManager.refreshSelection$.subscribe((event) => { + if (event) { + refreshEvents.push(event); + } + }); + + const result = await commandService.executeCommand(DocSelectAllCommand.id); + await awaitTime(0); + + expect(result).toBe(true); + expect(refreshEvents.at(-1)).toEqual(expect.objectContaining({ + unitId: 'test-doc', + subUnitId: 'test-doc', + isEditing: false, + docRanges: [ expect.objectContaining({ startOffset: 5, endOffset: 19, rangeType: 'RECT', }), - expect.objectContaining({ - startOffset: 24, - endOffset: 26, - rangeType: 'TEXT', - }), ], })); diff --git a/packages/docs-ui/src/commands/commands/doc-block-move.command.ts b/packages/docs-ui/src/commands/commands/doc-block-move.command.ts new file mode 100644 index 0000000000..753a376c62 --- /dev/null +++ b/packages/docs-ui/src/commands/commands/doc-block-move.command.ts @@ -0,0 +1,267 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentDataModel, ICommand, IDocumentBody, IDocumentData, JSONXActions } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import type { ITextRangeWithStyle } from '@univerjs/engine-render'; +import { CommandType, ICommandService, IUniverInstanceService, JSONX, Tools, UniverInstanceType } from '@univerjs/core'; +import { RichTextEditingMutation } from '@univerjs/docs'; + +export interface IMoveDocBlockCommandParams { + unitId?: string; + sourceRange: { + startOffset: number; + endOffset: number; + }; + targetOffset: number; +} + +export interface IMoveDocBlockActionResult { + nextDocumentData: IDocumentData; + movedRange: { + startOffset: number; + endOffset: number; + }; +} + +export const MoveDocBlockCommand: ICommand = { + id: 'doc.command.move-block', + type: CommandType.COMMAND, + handler: async (accessor, params) => { + if (!params) { + return false; + } + + const univerInstanceService = accessor.get(IUniverInstanceService); + const commandService = accessor.get(ICommandService); + const doc = (params.unitId + ? univerInstanceService.getUnit(params.unitId, UniverInstanceType.UNIVER_DOC) + : univerInstanceService.getCurrentUniverDocInstance()) as DocumentDataModel | undefined; + + if (!doc) { + return false; + } + + const previousDocumentData = doc.getSnapshot(); + const { nextDocumentData, movedRange } = buildMoveDocBlockActions({ + documentData: previousDocumentData, + sourceRange: params.sourceRange, + targetOffset: params.targetOffset, + }); + const actions = buildReplaceDocumentBodyActions(previousDocumentData, nextDocumentData); + + if (!actions) { + return false; + } + + const textRanges: ITextRangeWithStyle[] = [{ + startOffset: movedRange.startOffset, + endOffset: movedRange.endOffset, + collapsed: false, + }]; + + return Boolean(commandService.syncExecuteCommand(RichTextEditingMutation.id, { + unitId: doc.getUnitId(), + actions, + textRanges, + })); + }, +}; + +export function buildMoveDocBlockActions(params: { + documentData: IDocumentData; + sourceRange: { + startOffset: number; + endOffset: number; + }; + targetOffset: number; +}): IMoveDocBlockActionResult { + const nextDocumentData = Tools.deepClone(params.documentData); + const body = nextDocumentData.body; + + if (!body?.dataStream) { + return { + nextDocumentData, + movedRange: params.sourceRange, + }; + } + + const dataStreamLength = body.dataStream.length; + const startOffset = clamp(params.sourceRange.startOffset, 0, dataStreamLength); + const endOffset = clamp(params.sourceRange.endOffset, startOffset, dataStreamLength); + const targetOffset = clamp(params.targetOffset, 0, dataStreamLength); + + if (startOffset === endOffset || (targetOffset >= startOffset && targetOffset <= endOffset)) { + return { + nextDocumentData, + movedRange: { + startOffset, + endOffset, + }, + }; + } + + const movingText = body.dataStream.slice(startOffset, endOffset); + const moveLength = movingText.length; + const insertOffset = targetOffset > endOffset ? targetOffset - moveLength : targetOffset; + const withoutMovingText = body.dataStream.slice(0, startOffset) + body.dataStream.slice(endOffset); + body.dataStream = withoutMovingText.slice(0, insertOffset) + movingText + withoutMovingText.slice(insertOffset); + + remapBodyIndexesAfterMove(body, startOffset, endOffset, targetOffset, insertOffset, moveLength); + + return { + nextDocumentData, + movedRange: { + startOffset: insertOffset, + endOffset: insertOffset + moveLength, + }, + }; +} + +function buildReplaceDocumentBodyActions(previousDocumentData: IDocumentData, nextDocumentData: IDocumentData): JSONXActions | null { + const jsonX = JSONX.getInstance(); + const previousBody = previousDocumentData.body; + const nextBody = nextDocumentData.body; + + if (!previousBody || !nextBody) { + return null; + } + + const rawActions = [ + jsonX.replaceOp(['body', 'dataStream'], previousBody.dataStream, nextBody.dataStream), + jsonX.replaceOp(['body', 'paragraphs'], previousBody.paragraphs, nextBody.paragraphs), + jsonX.replaceOp(['body', 'sectionBreaks'], previousBody.sectionBreaks, nextBody.sectionBreaks), + jsonX.replaceOp(['body', 'tables'], previousBody.tables, nextBody.tables), + jsonX.replaceOp(['body', 'customBlocks'], previousBody.customBlocks, nextBody.customBlocks), + jsonX.replaceOp(['body', 'blockRanges'], previousBody.blockRanges, nextBody.blockRanges), + jsonX.replaceOp(['body', 'customRanges'], previousBody.customRanges, nextBody.customRanges), + jsonX.replaceOp(['body', 'customDecorations'], previousBody.customDecorations, nextBody.customDecorations), + jsonX.replaceOp(['body', 'textRuns'], previousBody.textRuns, nextBody.textRuns), + ].filter(Boolean) as JSONXActions[]; + + return rawActions.reduce((acc, cur) => JSONX.compose(acc, cur), null as JSONXActions); +} + +function remapBodyIndexesAfterMove( + body: IDocumentBody, + startOffset: number, + endOffset: number, + targetOffset: number, + insertOffset: number, + moveLength: number +) { + body.paragraphs = body.paragraphs?.map((paragraph) => ({ + ...paragraph, + startIndex: remapIndexAfterMove(paragraph.startIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + })).sort((left, right) => left.startIndex - right.startIndex); + + body.sectionBreaks = body.sectionBreaks?.map((sectionBreak) => ({ + ...sectionBreak, + startIndex: remapIndexAfterMove(sectionBreak.startIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + })).sort((left, right) => left.startIndex - right.startIndex); + + body.customBlocks = body.customBlocks?.map((customBlock) => ({ + ...customBlock, + startIndex: remapIndexAfterMove(customBlock.startIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + })).sort((left, right) => left.startIndex - right.startIndex); + + body.tables = body.tables?.map((table) => remapExclusiveRange(table, startOffset, endOffset, targetOffset, insertOffset, moveLength)) + .sort((left, right) => left.startIndex - right.startIndex); + + body.blockRanges = body.blockRanges?.map((blockRange) => remapInclusiveRange(blockRange, startOffset, endOffset, targetOffset, insertOffset, moveLength)) + .sort((left, right) => left.startIndex - right.startIndex); + + body.customRanges = body.customRanges?.map((customRange) => remapInclusiveRange(customRange, startOffset, endOffset, targetOffset, insertOffset, moveLength)) + .sort((left, right) => left.startIndex - right.startIndex); + + body.customDecorations = body.customDecorations?.map((customDecoration) => remapInclusiveRange(customDecoration, startOffset, endOffset, targetOffset, insertOffset, moveLength)) + .sort((left, right) => left.startIndex - right.startIndex); + + body.textRuns = body.textRuns?.map((textRun) => { + const remapped = remapExclusiveRange({ startIndex: textRun.st, endIndex: textRun.ed }, startOffset, endOffset, targetOffset, insertOffset, moveLength); + return { + ...textRun, + st: remapped.startIndex, + ed: remapped.endIndex, + }; + }).sort((left, right) => left.st - right.st); +} + +function remapExclusiveRange( + range: T, + startOffset: number, + endOffset: number, + targetOffset: number, + insertOffset: number, + moveLength: number +): T { + if (range.startIndex >= startOffset && range.endIndex <= endOffset) { + return { + ...range, + startIndex: insertOffset + range.startIndex - startOffset, + endIndex: insertOffset + range.endIndex - startOffset, + }; + } + + return { + ...range, + startIndex: remapIndexAfterMove(range.startIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + endIndex: remapIndexAfterMove(range.endIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + }; +} + +function remapInclusiveRange( + range: T, + startOffset: number, + endOffset: number, + targetOffset: number, + insertOffset: number, + moveLength: number +): T { + if (range.startIndex >= startOffset && range.endIndex < endOffset) { + return { + ...range, + startIndex: insertOffset + range.startIndex - startOffset, + endIndex: insertOffset + range.endIndex - startOffset, + }; + } + + return { + ...range, + startIndex: remapIndexAfterMove(range.startIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + endIndex: remapIndexAfterMove(range.endIndex, startOffset, endOffset, targetOffset, insertOffset, moveLength), + }; +} + +function remapIndexAfterMove(index: number, startOffset: number, endOffset: number, targetOffset: number, insertOffset: number, moveLength: number): number { + if (index >= startOffset && index < endOffset) { + return insertOffset + index - startOffset; + } + + if (targetOffset < startOffset && index >= targetOffset && index < startOffset) { + return index + moveLength; + } + + if (targetOffset > endOffset && index >= endOffset && index < targetOffset) { + return index - moveLength; + } + + return index; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} diff --git a/packages/docs-ui/src/commands/commands/doc-delete.command.ts b/packages/docs-ui/src/commands/commands/doc-delete.command.ts index 26d6e1fe67..5387e05922 100644 --- a/packages/docs-ui/src/commands/commands/doc-delete.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-delete.command.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { DocumentDataModel, ICommand, IMutationInfo, IParagraph, ITextRange, JSONXActions, Nullable } from '@univerjs/core'; +import type { DocumentDataModel, ICommand, IDocumentBlockRange, IDocumentBody, IMutationInfo, IParagraph, ITextRange, JSONXActions, Nullable } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IRectRangeWithStyle, ITextRangeWithStyle } from '@univerjs/engine-render'; import { @@ -22,10 +22,12 @@ import { BuildTextUtils, CommandType, DataStreamTreeTokenType, + HorizontalAlign, ICommandService, IUniverInstanceService, JSONX, PositionedObjectLayoutType, + sequenceExecuteAsync, TextX, TextXActionType, Tools, @@ -35,6 +37,7 @@ import { import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { getParagraphByGlyph, hasListGlyph, isFirstGlyph, isIndentByGlyph } from '@univerjs/engine-render'; +import { DocAutoFormatService } from '../../services/doc-auto-format.service'; import { DeleteDirection } from '../../types/delete-direction'; import { getCommandSkeleton, getRichTextEditPath } from '../util'; import { CutContentCommand } from './clipboard.inner.command'; @@ -401,6 +404,21 @@ export function getCursorWhenDelete(textRanges: Readonly): number { + return body.dataStream[blockRange.endIndex] === DataStreamTreeTokenType.BLOCK_END + ? blockRange.endIndex + : body.dataStream[blockRange.endIndex + 1] === DataStreamTreeTokenType.BLOCK_END + ? blockRange.endIndex + 1 + : blockRange.endIndex; +} + +export function isDeleteOffsetInsideBlockRange(body: IDocumentBody, offset: number): boolean { + return body.blockRanges?.some((blockRange) => { + const endTokenOffset = getBlockRangeEndTokenOffset(body, blockRange); + return blockRange.startIndex < offset && offset < endTokenOffset; + }) ?? false; +} + // Handle BACKSPACE key. export const DeleteLeftCommand: ICommand = { id: 'doc.command.delete-left', @@ -449,6 +467,11 @@ export const DeleteLeftCommand: ICommand = { return false; } + const autoFormatResult = await executeDeleteAutoFormat(accessor, DeleteLeftCommand.id); + if (autoFormatResult != null) { + return autoFormatResult; + } + const { segmentId, style, segmentPage } = activeRange; const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); @@ -459,6 +482,13 @@ export const DeleteLeftCommand: ICommand = { const actualRange = activeRange; const { startOffset, collapsed } = actualRange; + if (collapsed) { + const emptyCenteredParagraph = getEmptyCenteredParagraphAtOffset(body, startOffset); + if (emptyCenteredParagraph != null) { + return resetEmptyCenteredParagraphAlignment(commandService, docDataModel, segmentId ?? '', emptyCenteredParagraph, style); + } + } + const curGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage); // is in bullet list? @@ -470,9 +500,10 @@ export const DeleteLeftCommand: ICommand = { // Get the deleted glyph. It maybe null or undefined when the curGlyph is first glyph in skeleton. const preGlyph = skeleton.findNodeByCharIndex(startOffset - 1, segmentId, segmentPage); + const isInBlockRange = isDeleteOffsetInsideBlockRange(body, startOffset); const isUpdateParagraph = - isFirstGlyph(curGlyph) && preGlyph !== curGlyph && (isBullet === true || isIndent === true); + !isInBlockRange && isFirstGlyph(curGlyph) && preGlyph !== curGlyph && (isBullet === true || isIndent === true); if (isUpdateParagraph && collapsed) { const paragraph = getParagraphByGlyph(curGlyph, body); @@ -669,6 +700,11 @@ export const DeleteRightCommand: ICommand = { return false; } + const autoFormatResult = await executeDeleteAutoFormat(accessor, DeleteRightCommand.id); + if (autoFormatResult != null) { + return autoFormatResult; + } + const { segmentId, style, segmentPage } = activeRange; const body = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getBody(); @@ -678,6 +714,13 @@ export const DeleteRightCommand: ICommand = { const actualRange = activeRange; const { startOffset, endOffset, collapsed } = actualRange; + if (collapsed) { + const emptyCenteredParagraph = getEmptyCenteredParagraphAtOffset(body, startOffset); + if (emptyCenteredParagraph != null) { + return resetEmptyCenteredParagraphAlignment(commandService, docDataModel, segmentId ?? '', emptyCenteredParagraph, style); + } + } + // No need to delete when the cursor is at the last position of the last paragraph. if (startOffset === body.dataStream.length - 2 && collapsed) { return true; @@ -775,6 +818,20 @@ export const DeleteRightCommand: ICommand = { }, }; +async function executeDeleteAutoFormat(accessor: Parameters[0], commandId: string): Promise { + if (!accessor.has(DocAutoFormatService)) { + return null; + } + + const commandService = accessor.get(ICommandService); + const mutations = accessor.get(DocAutoFormatService).onAutoFormat(commandId); + if (!mutations.length) { + return null; + } + + return (await sequenceExecuteAsync(mutations, commandService)).result; +} + // get cursor position when BACKSPACE/DELETE excuse the CutContentCommand. function getTextRangesWhenDelete(activeRange: ITextRangeWithStyle, ranges: readonly ITextRange[]) { let cursor = activeRange.endOffset; @@ -802,6 +859,56 @@ function getTextRangesWhenDelete(activeRange: ITextRangeWithStyle, ranges: reado return textRanges; } +function getEmptyCenteredParagraphAtOffset(body: IDocumentBody, offset: number) { + const paragraphs = body.paragraphs ?? []; + for (let i = 0; i < paragraphs.length; i++) { + const paragraph = paragraphs[i]; + const paragraphTextStart = i === 0 ? 0 : paragraphs[i - 1].startIndex + 1; + if ( + paragraph.startIndex === paragraphTextStart && + paragraph.startIndex === offset && + paragraph.paragraphStyle?.horizontalAlign === HorizontalAlign.CENTER + ) { + return paragraph; + } + } +} + +function resetEmptyCenteredParagraphAlignment( + commandService: ICommandService, + docDataModel: DocumentDataModel, + segmentId: string, + paragraph: IParagraph, + style: ITextRangeWithStyle['style'] +) { + return commandService.executeCommand(UpdateCommand.id, { + unitId: docDataModel.getUnitId(), + updateBody: { + dataStream: '', + paragraphs: [{ + ...paragraph, + startIndex: 0, + paragraphStyle: { + ...paragraph.paragraphStyle, + horizontalAlign: HorizontalAlign.LEFT, + }, + }], + }, + range: { + startOffset: paragraph.startIndex, + endOffset: paragraph.startIndex + 1, + }, + textRanges: [{ + startOffset: paragraph.startIndex, + endOffset: paragraph.startIndex, + collapsed: true, + style, + }], + coverType: UpdateDocsAttributeType.REPLACE, + segmentId, + }); +} + export const DeleteCurrentParagraphCommand: ICommand = { id: 'doc.command.delete-current-paragraph', type: CommandType.COMMAND, diff --git a/packages/docs-ui/src/commands/commands/doc-horizontal-line.command.ts b/packages/docs-ui/src/commands/commands/doc-horizontal-line.command.ts index 1b5bf65364..746c3cd449 100644 --- a/packages/docs-ui/src/commands/commands/doc-horizontal-line.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-horizontal-line.command.ts @@ -15,7 +15,8 @@ */ import type { ICommand, ITextRangeParam } from '@univerjs/core'; -import { CommandType, DashStyleType, ICommandService } from '@univerjs/core'; +import { CommandType, DashStyleType, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import { DocContentInsertService } from '../../services/doc-content-insert.service'; import { BreakLineCommand } from './break-line.command'; import { getCurrentParagraph } from './util'; @@ -50,6 +51,22 @@ export const InsertHorizontalLineBellowCommand: ICommand { const commandService = accessor.get(ICommandService); + const doc = accessor.get(IUniverInstanceService).getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC); + let contentInsertRange: ReturnType = null; + try { + contentInsertRange = accessor.get(DocContentInsertService).consumeInsertRange(doc?.getUnitId()); + } catch { + contentInsertRange = null; + } + if (contentInsertRange) { + return commandService.syncExecuteCommand(HorizontalLineCommand.id, { + insertRange: { + startOffset: contentInsertRange.startOffset, + endOffset: contentInsertRange.endOffset, + }, + }); + } + const paragraph = getCurrentParagraph(accessor); if (!paragraph) { return false; diff --git a/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts b/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts index e4de7f056f..5d5ac76d61 100644 --- a/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-page-setup.command.ts @@ -14,20 +14,84 @@ * limitations under the License. */ -import type { DocumentDataModel, ICommand, ICommandInfo, ISize, JSONXActions, PageOrientType } from '@univerjs/core'; +import type { DocumentDataModel, ICommand, ICommandInfo, IDocumentStyle, ISize, ITable, ITables, JSONXActions, PageOrientType } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { CommandType, ICommandService, IUniverInstanceService, JSONX, UniverInstanceType } from '@univerjs/core'; +import { CommandType, DocumentFlavor, ICommandService, IUniverInstanceService, JSONX, MODERN_DOCUMENT_DEFAULT_MARGIN, UniverInstanceType } from '@univerjs/core'; import { RichTextEditingMutation } from '@univerjs/docs'; export interface IDocPageSetupCommandParams { pageSize: ISize; pageOrient: PageOrientType; + documentFlavor?: DocumentFlavor; marginTop: number; marginBottom: number; marginLeft: number; marginRight: number; } +const PAGE_FILL_TABLE_TOLERANCE = 2; + +interface IPageContentWidthConfig { + documentFlavor?: DocumentFlavor; + pageSize?: ISize; + marginLeft?: number; + marginRight?: number; +} + +function getPageContentWidth(config: IPageContentWidthConfig) { + const pageWidth = config.pageSize?.width ?? 0; + + if (!pageWidth) { + return 0; + } + + if (config.documentFlavor === DocumentFlavor.MODERN) { + return Math.max(0, pageWidth - MODERN_DOCUMENT_DEFAULT_MARGIN * 2); + } + + return Math.max(0, pageWidth - (config.marginLeft ?? 0) - (config.marginRight ?? 0)); +} + +function getTableColumnTotalWidth(table: ITable) { + return table.tableColumns.reduce((total, column) => total + (column.size.width.v ?? 0), 0); +} + +function isPageFillTable(table: ITable, oldContentWidth: number) { + if (oldContentWidth <= 0 || table.tableColumns.length === 0) { + return false; + } + + const columnTotalWidth = getTableColumnTotalWidth(table); + const tableWidth = columnTotalWidth || table.size.width.v; + return Math.abs(tableWidth - oldContentWidth) <= PAGE_FILL_TABLE_TOLERANCE; +} + +function resizePageFillTables(jsonX: JSONX, rawActions: NonNullable, tableSource: ITables | undefined, oldContentWidth: number, newContentWidth: number) { + if (!tableSource || oldContentWidth <= 0 || newContentWidth <= 0 || Math.abs(oldContentWidth - newContentWidth) <= PAGE_FILL_TABLE_TOLERANCE) { + return; + } + + Object.entries(tableSource).forEach(([tableId, table]) => { + if (!isPageFillTable(table, oldContentWidth)) { + return; + } + + const totalColumnWidth = getTableColumnTotalWidth(table); + if (totalColumnWidth <= 0) { + return; + } + + const tableWidthAction = jsonX.replaceOp(['tableSource', tableId, 'size', 'width', 'v'], table.size.width.v, newContentWidth); + tableWidthAction && rawActions.push(tableWidthAction); + + table.tableColumns.forEach((column, index) => { + const nextWidth = (column.size.width.v / totalColumnWidth) * newContentWidth; + const action = jsonX.replaceOp(['tableSource', tableId, 'tableColumns', index, 'size', 'width', 'v'], column.size.width.v, nextWidth); + action && rawActions.push(action); + }); + }); +} + export const DocPageSetupCommand: ICommand = { id: 'docs.command.page-setup', type: CommandType.COMMAND, @@ -38,9 +102,20 @@ export const DocPageSetupCommand: ICommand = { const commandService = accessor.get(ICommandService); const docDataModel = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC); if (!docDataModel) return false; - const { marginLeft, marginRight, marginBottom, marginTop, pageOrient, pageSize } = params; + const { documentFlavor, marginLeft, marginRight, marginBottom, marginTop, pageOrient, pageSize } = params; const jsonX = JSONX.getInstance(); const documentStyle = docDataModel.getDocumentStyle(); + const snapshot = docDataModel.getSnapshot(); + const newDocumentStyle: IDocumentStyle = { + ...documentStyle, + documentFlavor: documentFlavor ?? documentStyle.documentFlavor, + marginBottom, + marginLeft, + marginRight, + marginTop, + pageOrient, + pageSize, + }; const { marginBottom: oldMarginBottom, marginLeft: oldMarginLeft, @@ -48,8 +123,27 @@ export const DocPageSetupCommand: ICommand = { marginTop: oldMarginTop, pageOrient: oldPageOrient, pageSize: oldPageSize, + documentFlavor: oldDocumentFlavor, } = documentStyle; - const rawActions: JSONXActions = []; + const rawActions: NonNullable = []; + + resizePageFillTables( + jsonX, + rawActions, + snapshot.tableSource, + getPageContentWidth(documentStyle), + getPageContentWidth(newDocumentStyle) + ); + + if (documentFlavor !== undefined) { + if (oldDocumentFlavor === undefined) { + const action = jsonX.insertOp(['documentStyle', 'documentFlavor'], documentFlavor); + action && rawActions.push(action); + } else { + const action = jsonX.replaceOp(['documentStyle', 'documentFlavor'], oldDocumentFlavor, documentFlavor); + action && rawActions.push(action); + } + } if (oldMarginBottom === undefined) { const action = jsonX.insertOp(['documentStyle', 'marginBottom'], marginBottom); diff --git a/packages/docs-ui/src/commands/commands/doc-select-all.command.ts b/packages/docs-ui/src/commands/commands/doc-select-all.command.ts index ff7d0a88c3..82e57ce7ab 100644 --- a/packages/docs-ui/src/commands/commands/doc-select-all.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-select-all.command.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { DocumentDataModel, ICommand } from '@univerjs/core'; +import type { DocumentDataModel, ICommand, ICustomTable, IDocumentBody, IParagraph } from '@univerjs/core'; import type { ISuccinctDocRangeParam } from '@univerjs/engine-render'; import { CommandType, DOC_RANGE_TYPE, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { DocSelectionManagerService } from '@univerjs/docs'; @@ -41,40 +41,14 @@ export const DocSelectAllCommand: ICommand = { return false; } - const { tables = [], dataStream } = body; + const { dataStream } = body; if (dataStream === '\r\n') { return true; } - const textRanges: ISuccinctDocRangeParam[] = []; - let offset = 0; - for (const table of tables) { - const { startIndex, endIndex } = table; - if (offset !== startIndex) { - textRanges.push({ - startOffset: offset, - endOffset: startIndex - 1, - rangeType: DOC_RANGE_TYPE.TEXT, - }); - } - - // Push the rect range. - textRanges.push({ - startOffset: startIndex + 3, // 3 is TABLE_START, ROW_START, CELL_START. - endOffset: endIndex - 5, // 4 is CELL_END, ROW_END, TABLE_END AND \n. - rangeType: DOC_RANGE_TYPE.RECT, - }); - - offset = endIndex; - } - - if (offset !== body.dataStream.length - 2) { - textRanges.push({ - startOffset: offset, - endOffset: body.dataStream.length - 2, - rangeType: DOC_RANGE_TYPE.TEXT, - }); - } + const wholeDocRanges = getWholeDocumentRanges(body); + const scopedRange = getScopedSelectAllRange(body, activeRange); + const textRanges = scopedRange && !isSameRanges(docRanges, scopedRange) ? scopedRange : wholeDocRanges; docSelectionManagerService.replaceDocRanges(textRanges, { unitId, @@ -84,3 +58,187 @@ export const DocSelectAllCommand: ICommand = { return true; }, }; + +function getWholeDocumentRanges(body: IDocumentBody): ISuccinctDocRangeParam[] { + const textRanges: ISuccinctDocRangeParam[] = []; + let offset = 0; + + for (const table of body.tables ?? []) { + const { startIndex, endIndex } = table; + if (offset !== startIndex) { + textRanges.push(...getTextRangesByParagraphs(body, offset, startIndex - 1)); + } + + textRanges.push(getTableRectRange(table)); + offset = endIndex; + } + + if (offset !== body.dataStream.length - 2) { + textRanges.push(...getTextRangesByParagraphs(body, offset, body.dataStream.length - 2)); + } + + return textRanges; +} + +function getTextRangesByParagraphs(body: IDocumentBody, startOffset: number, endOffset: number): ISuccinctDocRangeParam[] { + if (startOffset > endOffset) { + return []; + } + + const paragraphs = [...(body.paragraphs ?? [])].sort((left, right) => left.startIndex - right.startIndex); + const ranges: ISuccinctDocRangeParam[] = []; + let offset = startOffset; + + for (const paragraph of paragraphs) { + if (paragraph.startIndex < offset) { + continue; + } + + if (paragraph.startIndex > endOffset) { + break; + } + + ranges.push({ + startOffset: offset, + endOffset: paragraph.startIndex, + rangeType: DOC_RANGE_TYPE.TEXT, + }); + offset = paragraph.startIndex + 1; + } + + if (offset <= endOffset) { + ranges.push({ + startOffset: offset, + endOffset, + rangeType: DOC_RANGE_TYPE.TEXT, + }); + } + + return ranges; +} + +function getScopedSelectAllRange(body: IDocumentBody, activeRange: ISuccinctDocRangeParam): ISuccinctDocRangeParam[] | null { + const startOffset = activeRange.startOffset; + const endOffset = activeRange.endOffset; + if (startOffset == null || endOffset == null) { + return null; + } + + const table = (body.tables ?? []).find((item) => isRangeInside(startOffset, endOffset, item.startIndex, item.endIndex)); + if (table) { + return [getTableRectRange(table)]; + } + + const customBlock = (body.customBlocks ?? []).find((item) => item.startIndex >= startOffset && item.startIndex <= endOffset); + if (customBlock) { + return [{ + endOffset: customBlock.startIndex, + rangeType: DOC_RANGE_TYPE.TEXT, + startOffset: customBlock.startIndex, + }]; + } + + const blockRange = (body.blockRanges ?? []).find((item) => isRangeInside(startOffset, endOffset, item.startIndex, item.endIndex)); + if (blockRange) { + return [{ + endOffset: Math.max(blockRange.startIndex + 1, blockRange.endIndex - 1), + rangeType: DOC_RANGE_TYPE.TEXT, + startOffset: blockRange.startIndex + 1, + }]; + } + + const paragraphRange = clampParagraphRangeByTables(getParagraphRangeAtOffset(body.paragraphs ?? [], startOffset), body.tables ?? [], startOffset); + return paragraphRange + ? [{ + ...paragraphRange, + rangeType: DOC_RANGE_TYPE.TEXT, + }] + : null; +} + +function clampParagraphRangeByTables( + paragraphRange: Pick | null, + tables: ICustomTable[], + activeOffset: number +): Pick | null { + if (!paragraphRange) { + return null; + } + + const nextRange = { ...paragraphRange }; + for (const table of tables) { + if (activeOffset < table.startIndex && nextRange.startOffset! < table.startIndex && table.startIndex <= nextRange.endOffset!) { + nextRange.endOffset = table.startIndex - 1; + } else if (activeOffset >= table.endIndex && nextRange.startOffset! <= table.endIndex && table.endIndex < nextRange.endOffset!) { + nextRange.startOffset = table.endIndex; + } + } + + return nextRange.startOffset! <= nextRange.endOffset! ? nextRange : null; +} + +function getTableRectRange(table: ICustomTable): ISuccinctDocRangeParam { + return { + startOffset: table.startIndex + 3, + endOffset: table.endIndex - 5, + rangeType: DOC_RANGE_TYPE.RECT, + }; +} + +function getParagraphRangeAtOffset(paragraphs: IParagraph[], offset: number): Pick | null { + const sortedParagraphs = [...paragraphs].sort((left, right) => left.startIndex - right.startIndex); + for (let index = 0; index < sortedParagraphs.length; index++) { + const paragraph = sortedParagraphs[index]; + const startOffset = index === 0 ? 0 : sortedParagraphs[index - 1].startIndex + 1; + if (startOffset <= offset && offset <= paragraph.startIndex) { + return { + endOffset: paragraph.startIndex, + startOffset, + }; + } + } + + return null; +} + +function isRangeInside(startOffset: number, endOffset: number, scopeStart: number, scopeEnd: number): boolean { + return startOffset >= scopeStart && endOffset <= scopeEnd; +} + +function isSameRanges(currentRanges: ISuccinctDocRangeParam[], nextRanges: ISuccinctDocRangeParam[]): boolean { + if (currentRanges.length !== nextRanges.length) { + return isSameTextRangeCoverage(currentRanges, nextRanges); + } + + return currentRanges.every((currentRange, index) => { + const nextRange = nextRanges[index]; + return currentRange.startOffset === nextRange.startOffset && + currentRange.endOffset === nextRange.endOffset && + getRangeType(currentRange) === getRangeType(nextRange); + }); +} + +function isSameTextRangeCoverage(currentRanges: ISuccinctDocRangeParam[], nextRanges: ISuccinctDocRangeParam[]): boolean { + if (nextRanges.length !== 1 || getRangeType(nextRanges[0]) !== DOC_RANGE_TYPE.TEXT) { + return false; + } + + const targetRange = nextRanges[0]; + if (targetRange.startOffset == null || targetRange.endOffset == null) { + return false; + } + + if (currentRanges.some((range) => getRangeType(range) !== DOC_RANGE_TYPE.TEXT || range.startOffset == null || range.endOffset == null)) { + return false; + } + + const startOffset = Math.min(...currentRanges.map((range) => range.startOffset!)); + const endOffset = Math.max(...currentRanges.map((range) => range.endOffset!)); + return startOffset === targetRange.startOffset && + endOffset === targetRange.endOffset && + currentRanges.every((range) => range.startOffset! >= targetRange.startOffset! && range.endOffset! <= targetRange.endOffset!); +} + +function getRangeType(range: ISuccinctDocRangeParam): DOC_RANGE_TYPE { + return range.rangeType ?? DOC_RANGE_TYPE.TEXT; +} diff --git a/packages/docs-ui/src/commands/commands/inline-format.command.ts b/packages/docs-ui/src/commands/commands/inline-format.command.ts index dd83e4014c..80b263248d 100644 --- a/packages/docs-ui/src/commands/commands/inline-format.command.ts +++ b/packages/docs-ui/src/commands/commands/inline-format.command.ts @@ -253,7 +253,10 @@ export const SetInlineFormatCommand: ICommand = { const univerInstanceService = accessor.get(IUniverInstanceService); const docMenuStyleService = accessor.get(DocMenuStyleService); - const docRanges = docSelectionManagerService.getDocRanges(); + const textRanges = docSelectionManagerService.getTextRanges() ?? []; + const docRanges = textRanges.length > 0 + ? textRanges.filter((range) => range.startOffset != null && range.endOffset != null) + : docSelectionManagerService.getDocRanges(); const activeRange = docRanges.find((r) => r.isActive) ?? docRanges[0]; if (docRanges.length === 0) { diff --git a/packages/docs-ui/src/commands/commands/list.command.ts b/packages/docs-ui/src/commands/commands/list.command.ts index e0ad91cdbc..14fdf86b5e 100644 --- a/packages/docs-ui/src/commands/commands/list.command.ts +++ b/packages/docs-ui/src/commands/commands/list.command.ts @@ -33,6 +33,7 @@ import { UniverInstanceType, } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { DocContentInsertService } from '../../services/doc-content-insert.service'; import { getRichTextEditPath } from '../util'; import { getCurrentParagraph } from './util'; @@ -345,6 +346,7 @@ export const ToggleCheckListCommand: ICommand = { interface IOrderListCommandParams { value?: PresetListType; + docRange?: ITextRangeWithStyle[]; } export const OrderListCommand: ICommand = { @@ -357,11 +359,13 @@ export const OrderListCommand: ICommand = { if (params?.value) { return commandService.syncExecuteCommand(ChangeListTypeCommand.id, { listType: params.value, + docRange: params.docRange, }); } return commandService.syncExecuteCommand(ListOperationCommand.id, { listType: PresetListType.ORDER_LIST, + docRange: params?.docRange, }); }, }; @@ -483,19 +487,28 @@ export const QuickListCommand: ICommand = { function insertList(accessor: IAccessor, listType: PresetListType) { const commandService = accessor.get(ICommandService); - const paragraph = getCurrentParagraph(accessor); - if (!paragraph) { - return false; - } const docDataModel = accessor.get(IUniverInstanceService).getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC); if (!docDataModel) { return false; } + let contentInsertRange: ReturnType = null; + try { + contentInsertRange = accessor.get(DocContentInsertService).consumeInsertRange(docDataModel.getUnitId()); + } catch { + contentInsertRange = null; + } + const paragraph = contentInsertRange ? null : getCurrentParagraph(accessor); + if (!contentInsertRange && !paragraph) { + return false; + } + const sourceParagraph = paragraph || undefined; + const insertOffset = contentInsertRange?.startOffset ?? sourceParagraph!.startIndex + 1; + const sourceBullet = sourceParagraph?.bullet; const textX = BuildTextUtils.selection.replace({ doc: docDataModel, selection: { - startOffset: paragraph.startIndex + 1, - endOffset: paragraph.startIndex + 1, + startOffset: insertOffset, + endOffset: contentInsertRange?.endOffset ?? insertOffset, collapsed: true, }, body: { @@ -504,12 +517,12 @@ function insertList(accessor: IAccessor, listType: PresetListType) { { startIndex: 0, paragraphStyle: { - ...paragraph.paragraphStyle, + ...(sourceParagraph?.paragraphStyle ?? {}), }, bullet: { listType, - listId: paragraph.bullet?.listType === listType ? paragraph.bullet.listId : generateRandomId(6), - nestingLevel: paragraph.bullet?.listType === listType ? paragraph.bullet.nestingLevel : 0, + listId: sourceBullet?.listType === listType ? sourceBullet.listId : generateRandomId(6), + nestingLevel: sourceBullet?.listType === listType ? sourceBullet.nestingLevel : 0, }, }, ], @@ -525,8 +538,8 @@ function insertList(accessor: IAccessor, listType: PresetListType) { unitId: docDataModel.getUnitId(), actions: [], textRanges: [{ - startOffset: paragraph.startIndex + 1, - endOffset: paragraph.startIndex + 1, + startOffset: insertOffset, + endOffset: insertOffset, collapsed: true, }], isEditing: false, diff --git a/packages/docs-ui/src/commands/commands/set-heading.command.ts b/packages/docs-ui/src/commands/commands/set-heading.command.ts index e9d4ce849c..5d5beaae2e 100644 --- a/packages/docs-ui/src/commands/commands/set-heading.command.ts +++ b/packages/docs-ui/src/commands/commands/set-heading.command.ts @@ -19,6 +19,7 @@ import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { ITextRangeWithStyle } from '@univerjs/engine-render'; import { BuildTextUtils, CommandType, generateRandomId, ICommandService, IUniverInstanceService, JSONX, NamedStyleType, TextX, TextXActionType, UniverInstanceType } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { DocContentInsertService } from '../../services/doc-content-insert.service'; import { getRichTextEditPath } from '../util'; export interface ISetParagraphNamedStyleCommandParams { @@ -41,6 +42,11 @@ export const SetParagraphNamedStyleCommand: ICommand[0], unitId: string) { + try { + return accessor.get(DocContentInsertService).consumeInsertRange(unitId); + } catch { + return null; + } +} + +function insertNamedStyleParagraph( + accessor: Parameters[0], + doc: DocumentDataModel, + namedStyleType: NamedStyleType, + startOffset: number, + endOffset: number +): boolean { + const textX = BuildTextUtils.selection.replace({ + doc, + selection: { + startOffset, + endOffset, + collapsed: startOffset === endOffset, + }, + body: { + dataStream: '\r', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + namedStyleType, + headingId: !namedStyleType || namedStyleType === NamedStyleType.NORMAL_TEXT ? undefined : generateRandomId(6), + }, + }], + }, + }); + + if (!textX) { + return false; + } + + const jsonX = JSONX.getInstance(); + const commandService = accessor.get(ICommandService); + return Boolean(commandService.syncExecuteCommand(RichTextEditingMutation.id, { + actions: jsonX.editOp(textX.serialize(), getRichTextEditPath(doc)), + isEditing: false, + textRanges: [{ + startOffset, + endOffset: startOffset, + collapsed: true, + }], + unitId: doc.getUnitId(), + })); +} + export const QuickHeadingCommand: ICommand = { id: 'doc.command.quick-heading', type: CommandType.COMMAND, diff --git a/packages/docs-ui/src/commands/commands/table/doc-table-create.command.ts b/packages/docs-ui/src/commands/commands/table/doc-table-create.command.ts index 153cfe9303..f1e1d7516b 100644 --- a/packages/docs-ui/src/commands/commands/table/doc-table-create.command.ts +++ b/packages/docs-ui/src/commands/commands/table/doc-table-create.command.ts @@ -20,6 +20,7 @@ import type { ITextRangeWithStyle } from '@univerjs/engine-render'; import { CommandType, DataStreamTreeTokenType, ICommandService, IUniverInstanceService, JSONX, TextX, TextXActionType } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { getTextRunAtPosition } from '../../../basics/paragraph'; +import { DocContentInsertService } from '../../../services/doc-content-insert.service'; import { DocMenuStyleService } from '../../../services/doc-menu-style.service'; import { getCommandSkeleton, getRichTextEditPath } from '../../util'; import { generateParagraphs } from '../break-line.command'; @@ -46,14 +47,25 @@ export const CreateDocTableCommand: ICommand = { const commandService = accessor.get(ICommandService); const docMenuStyleService = accessor.get(DocMenuStyleService); - const activeRange = docSelectionManagerService.getActiveTextRange(); - if (activeRange == null) { + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + if (docDataModel == null) { return false; } - const { segmentId, segmentPage } = activeRange; - const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + let contentInsertRange: ReturnType = null; + try { + contentInsertRange = accessor.get(DocContentInsertService).consumeInsertRange(docDataModel.getUnitId()); + } catch { + contentInsertRange = null; + } + + const activeRange = docSelectionManagerService.getActiveTextRange(); + if (activeRange == null && contentInsertRange == null) { + return false; + } + const segmentId = contentInsertRange?.segmentId ?? activeRange?.segmentId ?? ''; + const segmentPage = activeRange?.segmentPage; const body = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getBody(); - if (docDataModel == null || body == null) { + if (body == null) { return false; } @@ -64,7 +76,7 @@ export const CreateDocTableCommand: ICommand = { if (skeleton == null) { return false; } - const { startOffset } = activeRange; + const startOffset = contentInsertRange?.startOffset ?? activeRange!.startOffset; const paragraphs = body.paragraphs ?? []; const prevParagraph = paragraphs.find((p) => p.startIndex >= startOffset); diff --git a/packages/docs-ui/src/commands/operations/open-page-setting.operation.ts b/packages/docs-ui/src/commands/operations/open-page-setting.operation.ts index d46b8cf219..276bd8c0e5 100644 --- a/packages/docs-ui/src/commands/operations/open-page-setting.operation.ts +++ b/packages/docs-ui/src/commands/operations/open-page-setting.operation.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import type { ICommand, PaperType } from '@univerjs/core'; +import type { ICommand, ModernDocumentWidthMode, PaperType } from '@univerjs/core'; import type { IConfirmPartMethodOptions } from '@univerjs/ui'; import type { IDocPageSetupCommandParams } from '../commands/doc-page-setup.command'; -import { CommandType, ICommandService, IConfirmService, LocaleService, PAGE_SIZE } from '@univerjs/core'; +import { CommandType, DocumentFlavor, ICommandService, IConfirmService, LocaleService, MODERN_DOCUMENT_WIDTH, PAGE_SIZE } from '@univerjs/core'; import { PAGE_SETTING_COMPONENT_ID } from '../../views/page-settings'; import { DocPageSetupCommand } from '../commands/doc-page-setup.command'; @@ -43,8 +43,16 @@ export const DocOpenPageSettingCommand: ICommand = { onConfirm: (result) => { disposable.dispose(); if (!result) return; - const paperSize = PAGE_SIZE[result.paperSize as PaperType]; + + const paperSize = result.mode === DocumentFlavor.MODERN + ? { + width: MODERN_DOCUMENT_WIDTH[result.modernWidth as ModernDocumentWidthMode], + height: PAGE_SIZE.A4.height, + } + : PAGE_SIZE[result.paperSize as PaperType]; + commandService.executeCommand(DocPageSetupCommand.id, { + documentFlavor: result.mode, pageOrient: result.orientation, marginTop: result.margins.top, marginBottom: result.margins.bottom, diff --git a/packages/docs-ui/src/components/float-toolbar/FloatToolbar.stories.tsx b/packages/docs-ui/src/components/float-toolbar/FloatToolbar.stories.tsx index 44bd2afac9..983b2053c4 100644 --- a/packages/docs-ui/src/components/float-toolbar/FloatToolbar.stories.tsx +++ b/packages/docs-ui/src/components/float-toolbar/FloatToolbar.stories.tsx @@ -36,6 +36,7 @@ import { } from '../../commands/commands/inline-format.command'; import enUS from '../../locale/en-US'; import zhCN from '../../locale/zh-CN'; +import { FLOAT_TEXT_STYLE_MENU_ID } from '../../menu/menu'; import { menuSchema } from '../../menu/schema'; import { FloatToolbar } from './FloatToolbar'; @@ -95,6 +96,7 @@ export const Playground = {
{ function getRibbon(): void { - const flatMenus = menuManagerService.getFlatMenuByPositionKey(MenuManagerPosition.RIBBON); + const flatMenus = [ + ...menuManagerService.getFlatMenuByPositionKey(FLOAT_TOOLBAR_MENU_POSITION), + ...menuManagerService.getFlatMenuByPositionKey(MenuManagerPosition.RIBBON), + ]; const menus: IMenuSchema[] = []; for (const key of avaliableMenus) { diff --git a/packages/docs-ui/src/components/paragraph-menu/__tests__/paragraph-menu.spec.ts b/packages/docs-ui/src/components/paragraph-menu/__tests__/paragraph-menu.spec.ts new file mode 100644 index 0000000000..41d2930553 --- /dev/null +++ b/packages/docs-ui/src/components/paragraph-menu/__tests__/paragraph-menu.spec.ts @@ -0,0 +1,215 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NamedStyleType } from '@univerjs/core'; + +import { describe, expect, it } from 'vitest'; +import { getParagraphMenuActiveHeadingCommandId, getParagraphMenuCommand, getParagraphMenuHiddenHeadingCommandIds, getParagraphMenuIconSizeClass, getParagraphMenuPopupDirection, getParagraphMenuTargetRange, isEmptyParagraphMenuTarget, shouldShowParagraphSettingMenu, shouldUseInsertBelowRange } from '..'; +import { HorizontalLineCommand } from '../../../commands/commands/doc-horizontal-line.command'; +import { BulletListCommand, InsertBulletListBellowCommand, OrderListCommand } from '../../../commands/commands/list.command'; +import { H1HeadingCommand, H3HeadingCommand, H5HeadingCommand, NormalTextHeadingCommand, SetParagraphNamedStyleCommand, SubtitleHeadingCommand, TitleHeadingCommand } from '../../../commands/commands/set-heading.command'; +import { CreateDocTableCommand } from '../../../commands/commands/table/doc-table-create.command'; +import { + EmptyParagraphBulletListMenuItemFactory, + EmptyParagraphH1MenuItemFactory, + EmptyParagraphHorizontalLineMenuItemFactory, + HEADING_ICON_MAP, + INSERT_BELLOW_MENU_ID, + shouldShowParagraphHeadingOption, + TableBlockCopyMenuItemFactory, + TableBlockDeleteMenuItemFactory, + TableBlockPasteMenuItemFactory, +} from '../../../menu/paragraph-menu'; + +describe('ParagraphMenu', () => { + it('uses a smaller icon for normal text paragraph triggers', () => { + expect(getParagraphMenuIconSizeClass('TextTypeIcon')).toBe('univer-size-3'); + expect(getParagraphMenuIconSizeClass('TitleTypeIcon')).toBe('univer-size-4'); + expect(getParagraphMenuIconSizeClass('SubtitleTypeIcon')).toBe('univer-size-4'); + expect(getParagraphMenuIconSizeClass('H1Icon')).toBe('univer-size-4'); + expect(HEADING_ICON_MAP[NamedStyleType.TITLE].key).toBe('TitleTypeIcon'); + expect(HEADING_ICON_MAP[NamedStyleType.SUBTITLE].key).toBe('SubtitleTypeIcon'); + }); + + it('uses fully-qualified locale keys for paragraph context menu labels', () => { + const accessor = { + get: () => ({ + get: () => undefined, + register: () => undefined, + }), + } as never; + + expect(EmptyParagraphH1MenuItemFactory(accessor).title).toBe('ui.toolbar.heading.1'); + expect(EmptyParagraphBulletListMenuItemFactory(accessor).title).toBe('docs-ui.rightClick.bulletList'); + expect(EmptyParagraphHorizontalLineMenuItemFactory(accessor).title).toBe('docs-ui.toolbar.horizontalLine'); + expect(TableBlockCopyMenuItemFactory(accessor).title).toBe('docs-ui.rightClick.copy'); + expect(TableBlockPasteMenuItemFactory(accessor).title).toBe('docs-ui.rightClick.paste'); + expect(TableBlockDeleteMenuItemFactory(accessor).title).toBe('docs-ui.rightClick.delete'); + }); + + it('opens the popup away from the drag handle when there is not enough left space', () => { + expect(getParagraphMenuPopupDirection(170)).toBe('right'); + expect(getParagraphMenuPopupDirection(260)).toBe('left'); + }); + + it('shows title and subtitle heading shortcuts only when they are the current paragraph style', () => { + expect(shouldShowParagraphHeadingOption(NamedStyleType.HEADING_5, NamedStyleType.NORMAL_TEXT)).toBe(true); + expect(shouldShowParagraphHeadingOption(NamedStyleType.TITLE, NamedStyleType.NORMAL_TEXT)).toBe(false); + expect(shouldShowParagraphHeadingOption(NamedStyleType.SUBTITLE, NamedStyleType.NORMAL_TEXT)).toBe(false); + + expect(shouldShowParagraphHeadingOption(NamedStyleType.HEADING_5, NamedStyleType.TITLE)).toBe(false); + expect(shouldShowParagraphHeadingOption(NamedStyleType.TITLE, NamedStyleType.TITLE)).toBe(true); + expect(shouldShowParagraphHeadingOption(NamedStyleType.SUBTITLE, NamedStyleType.TITLE)).toBe(false); + + expect(shouldShowParagraphHeadingOption(NamedStyleType.HEADING_5, NamedStyleType.SUBTITLE)).toBe(false); + expect(shouldShowParagraphHeadingOption(NamedStyleType.TITLE, NamedStyleType.SUBTITLE)).toBe(false); + expect(shouldShowParagraphHeadingOption(NamedStyleType.SUBTITLE, NamedStyleType.SUBTITLE)).toBe(true); + }); + + it('maps paragraph named styles to the active heading menu item', () => { + expect(getParagraphMenuActiveHeadingCommandId(NamedStyleType.HEADING_1)).toBe(H1HeadingCommand.id); + expect(getParagraphMenuActiveHeadingCommandId(NamedStyleType.HEADING_3)).toBe(H3HeadingCommand.id); + expect(getParagraphMenuActiveHeadingCommandId(NamedStyleType.NORMAL_TEXT)).toBe(NormalTextHeadingCommand.id); + expect(getParagraphMenuActiveHeadingCommandId(undefined)).toBe(NormalTextHeadingCommand.id); + expect(getParagraphMenuActiveHeadingCommandId(NamedStyleType.TITLE)).toBe(TitleHeadingCommand.id); + expect(getParagraphMenuActiveHeadingCommandId(NamedStyleType.SUBTITLE)).toBe(SubtitleHeadingCommand.id); + }); + + it('hides the alternate title shortcuts for the hovered paragraph style', () => { + expect(getParagraphMenuHiddenHeadingCommandIds(NamedStyleType.TITLE)).toEqual([ + H5HeadingCommand.id, + SubtitleHeadingCommand.id, + ]); + expect(getParagraphMenuHiddenHeadingCommandIds(NamedStyleType.SUBTITLE)).toEqual([ + H5HeadingCommand.id, + TitleHeadingCommand.id, + ]); + expect(getParagraphMenuHiddenHeadingCommandIds(NamedStyleType.NORMAL_TEXT)).toEqual([ + TitleHeadingCommand.id, + SubtitleHeadingCommand.id, + ]); + }); + + it('shows paragraph settings only for paragraph and list menu targets', () => { + expect(shouldShowParagraphSettingMenu(null)).toBe(true); + expect(shouldShowParagraphSettingMenu({ kind: 'paragraph' } as never)).toBe(true); + expect(shouldShowParagraphSettingMenu({ kind: 'paragraph', icon: 'OrderIcon' } as never)).toBe(true); + expect(shouldShowParagraphSettingMenu({ kind: 'blockRange' } as never)).toBe(false); + expect(shouldShowParagraphSettingMenu({ kind: 'table' } as never)).toBe(false); + expect(shouldShowParagraphSettingMenu({ kind: 'customBlock' } as never)).toBe(false); + }); + + it('detects empty paragraph menu targets', () => { + const paragraph = { + paragraphStart: 2, + paragraphEnd: 2, + }; + const nonEmptyParagraph = { + paragraphStart: 2, + paragraphEnd: 3, + }; + + expect(isEmptyParagraphMenuTarget('a\r\r', paragraph as never)).toBe(true); + expect(isEmptyParagraphMenuTarget('a\r\r', { paragraphStart: 1, paragraphEnd: 2 } as never)).toBe(true); + expect(isEmptyParagraphMenuTarget('a\n\n', { paragraphStart: 1, paragraphEnd: 2 } as never)).toBe(true); + expect(isEmptyParagraphMenuTarget('a\rb\r', nonEmptyParagraph as never)).toBe(false); + }); + + it('builds a collapsed selection range for the hovered paragraph', () => { + expect(getParagraphMenuTargetRange({ + paragraphStart: 3, + paragraphEnd: 8, + segmentId: 'header-1', + } as never)).toEqual({ + collapsed: true, + endOffset: 3, + segmentId: 'header-1', + startOffset: 3, + }); + }); + + it('preserves context menu command params for paragraph menu actions', () => { + expect(getParagraphMenuCommand({ + commandId: CreateDocTableCommand.id, + id: 'doc.operation.create-table', + label: 'doc.operation.create-table', + params: { rowCount: 3, colCount: 5 }, + })).toEqual({ + commandId: CreateDocTableCommand.id, + params: { rowCount: 3, colCount: 5 }, + }); + + expect(getParagraphMenuCommand({ + label: 'doc.command.h1-heading', + }, { startOffset: 3, endOffset: 3, collapsed: true })).toEqual({ + commandId: SetParagraphNamedStyleCommand.id, + params: { + value: NamedStyleType.HEADING_1, + textRanges: [{ startOffset: 3, endOffset: 3, collapsed: true }], + }, + }); + }); + + it('passes the hovered paragraph range to current-paragraph menu commands', () => { + const targetRange = { startOffset: 3, endOffset: 3, collapsed: true }; + + expect(getParagraphMenuCommand({ + label: BulletListCommand.id, + }, targetRange)).toEqual({ + commandId: BulletListCommand.id, + params: { docRange: [targetRange] }, + }); + expect(getParagraphMenuCommand({ + label: OrderListCommand.id, + }, targetRange)).toEqual({ + commandId: OrderListCommand.id, + params: { docRange: [targetRange] }, + }); + expect(getParagraphMenuCommand({ + label: HorizontalLineCommand.id, + }, targetRange)).toEqual({ + commandId: HorizontalLineCommand.id, + params: { insertRange: targetRange }, + }); + expect(getParagraphMenuCommand({ + label: H1HeadingCommand.id, + }, null)).toEqual({ + commandId: H1HeadingCommand.id, + params: undefined, + }); + }); + + it('detects menu commands that should use the hovered block insert-below anchor', () => { + expect(shouldUseInsertBelowRange(InsertBulletListBellowCommand.id, { + id: InsertBulletListBellowCommand.id, + })).toBe(true); + expect(shouldUseInsertBelowRange('docs-callout.command.insert-below', { + id: 'docs-callout.command.insert-below', + })).toBe(true); + expect(shouldUseInsertBelowRange('doc.command.insert-float-image', { + id: 'doc.command.insert-float-image', + })).toBe(true); + expect(shouldUseInsertBelowRange(CreateDocTableCommand.id, { + id: 'doc.operation.create-table', + })).toBe(true); + expect(shouldUseInsertBelowRange(H1HeadingCommand.id, { + id: H1HeadingCommand.id, + })).toBe(false); + expect(shouldUseInsertBelowRange(H1HeadingCommand.id, { + id: INSERT_BELLOW_MENU_ID, + })).toBe(true); + }); +}); diff --git a/packages/docs-ui/src/components/paragraph-menu/index.tsx b/packages/docs-ui/src/components/paragraph-menu/index.tsx index 79801454ca..ff9e6730bb 100644 --- a/packages/docs-ui/src/components/paragraph-menu/index.tsx +++ b/packages/docs-ui/src/components/paragraph-menu/index.tsx @@ -15,37 +15,208 @@ */ import type { DocumentDataModel } from '@univerjs/core'; -import type { IPopup } from '@univerjs/ui'; -import { ICommandService, IUniverInstanceService, NamedStyleType, UniverInstanceType } from '@univerjs/core'; -import { borderClassName, clsx } from '@univerjs/design'; +import type { ITextRangeWithStyle } from '@univerjs/engine-render'; +import type { IPopup, IValueOption } from '@univerjs/ui'; +import type { IMutiPageParagraphBound } from '../../services/doc-event-manager.service'; +import type { IDocBlockMenuTarget } from '../../services/doc-paragraph-menu.service'; +import { ICommandService, IUniverInstanceService, NamedStyleType, SliceBodyType, UniverInstanceType } from '@univerjs/core'; +import { clsx } from '@univerjs/design'; +import { DocSelectionManagerService } from '@univerjs/docs'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { DownIcon } from '@univerjs/icons'; -import { ContextMenuPanel, ContextMenuPosition, ILayoutService, RectPopup, useDependency, useObservable } from '@univerjs/ui'; -import { useMemo, useRef, useState } from 'react'; +import { ComponentManager, ContextMenuPanel, ContextMenuPosition, IClipboardInterfaceService, ILayoutService, RectPopup, useDependency, useObservable } from '@univerjs/ui'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { HEADING_ICON_MAP } from '../../menu/paragraph-menu'; +import { DocCopyCommand, DocPasteCommand } from '../../commands/commands/clipboard.command'; +import { MoveDocBlockCommand } from '../../commands/commands/doc-block-move.command'; +import { HorizontalLineCommand } from '../../commands/commands/doc-horizontal-line.command'; +import { BulletListCommand, CheckListCommand, OrderListCommand } from '../../commands/commands/list.command'; +import { H1HeadingCommand, H2HeadingCommand, H3HeadingCommand, H4HeadingCommand, H5HeadingCommand, NormalTextHeadingCommand, SetParagraphNamedStyleCommand, SubtitleHeadingCommand, TitleHeadingCommand } from '../../commands/commands/set-heading.command'; +import { DocTableDeleteTableCommand } from '../../commands/commands/table/doc-table-delete.command'; +import { DocParagraphSettingPanelOperation } from '../../commands/operations/doc-paragraph-setting-panel.operation'; +import { DOC_TABLE_BLOCK_MENU_ID, EMPTY_PARAGRAPH_MENU_ID, HEADING_ICON_MAP, INSERT_BELLOW_MENU_ID } from '../../menu/paragraph-menu'; +import { IDocClipboardService } from '../../services/clipboard/clipboard.service'; +import { DocContentInsertService } from '../../services/doc-content-insert.service'; import { DocEventManagerService } from '../../services/doc-event-manager.service'; import { DocParagraphMenuService } from '../../services/doc-paragraph-menu.service'; +export function getParagraphMenuIconSizeClass(iconKey: string): string { + return iconKey === 'TextTypeIcon' ? 'univer-size-3' : 'univer-size-4'; +} + +export function getParagraphMenuPopupDirection(anchorLeft: number, menuWidth = 212, viewportPadding = 8): 'left' | 'right' { + return anchorLeft - menuWidth < viewportPadding ? 'right' : 'left'; +} + +export function isEmptyParagraphMenuTarget(dataStream: string, paragraph?: IMutiPageParagraphBound | null | void): boolean { + if (!paragraph) { + return false; + } + + return dataStream.slice(paragraph.paragraphStart, paragraph.paragraphEnd).replace(/[\r\n]/g, '') === ''; +} + +export function getParagraphMenuTargetRange(paragraph?: IMutiPageParagraphBound | null | void): ITextRangeWithStyle | null { + if (!paragraph) { + return null; + } + + const blockRange = (paragraph as IMutiPageParagraphBound & { blockRange?: { endIndex: number; startIndex: number } }).blockRange; + if (blockRange) { + return { + startOffset: blockRange.startIndex, + endOffset: blockRange.endIndex + 1, + collapsed: false, + segmentId: paragraph.segmentId, + }; + } + + return { + startOffset: paragraph.paragraphStart, + endOffset: paragraph.paragraphStart, + collapsed: true, + segmentId: paragraph.segmentId, + }; +} + +const HEADING_COMMAND_VALUES: Record = { + [H1HeadingCommand.id]: NamedStyleType.HEADING_1, + [H2HeadingCommand.id]: NamedStyleType.HEADING_2, + [H3HeadingCommand.id]: NamedStyleType.HEADING_3, + [H4HeadingCommand.id]: NamedStyleType.HEADING_4, + [H5HeadingCommand.id]: NamedStyleType.HEADING_5, + [NormalTextHeadingCommand.id]: NamedStyleType.NORMAL_TEXT, + [TitleHeadingCommand.id]: NamedStyleType.TITLE, + [SubtitleHeadingCommand.id]: NamedStyleType.SUBTITLE, +}; + +const NAMED_STYLE_HEADING_COMMAND_IDS: Partial> = { + [NamedStyleType.HEADING_1]: H1HeadingCommand.id, + [NamedStyleType.HEADING_2]: H2HeadingCommand.id, + [NamedStyleType.HEADING_3]: H3HeadingCommand.id, + [NamedStyleType.HEADING_4]: H4HeadingCommand.id, + [NamedStyleType.HEADING_5]: H5HeadingCommand.id, + [NamedStyleType.NORMAL_TEXT]: NormalTextHeadingCommand.id, + [NamedStyleType.TITLE]: TitleHeadingCommand.id, + [NamedStyleType.SUBTITLE]: SubtitleHeadingCommand.id, +}; + +export function getParagraphMenuActiveHeadingCommandId(namedStyleType?: NamedStyleType): string { + return NAMED_STYLE_HEADING_COMMAND_IDS[namedStyleType ?? NamedStyleType.NORMAL_TEXT] ?? NormalTextHeadingCommand.id; +} + +export function getParagraphMenuHiddenHeadingCommandIds(namedStyleType?: NamedStyleType): string[] { + if (namedStyleType === NamedStyleType.TITLE) { + return [H5HeadingCommand.id, SubtitleHeadingCommand.id]; + } + + if (namedStyleType === NamedStyleType.SUBTITLE) { + return [H5HeadingCommand.id, TitleHeadingCommand.id]; + } + + return [TitleHeadingCommand.id, SubtitleHeadingCommand.id]; +} + +export function getParagraphMenuCommand(params: IValueOption, targetRange?: ITextRangeWithStyle | null): { commandId?: string; params?: object } { + const commandId = params.commandId ?? params.id ?? (typeof params.label === 'string' ? params.label : undefined); + if (commandId && targetRange && commandId in HEADING_COMMAND_VALUES) { + return { + commandId: SetParagraphNamedStyleCommand.id, + params: { + value: HEADING_COMMAND_VALUES[commandId], + textRanges: [targetRange], + }, + }; + } + + if (commandId && targetRange && (commandId === BulletListCommand.id || commandId === OrderListCommand.id || commandId === CheckListCommand.id)) { + return { + commandId, + params: { + docRange: [targetRange], + }, + }; + } + + if (commandId === HorizontalLineCommand.id && targetRange) { + return { + commandId, + params: { + insertRange: targetRange, + }, + }; + } + + const fallbackParams = typeof params.params === 'function' ? params.params() : params.params; + const commandParams = typeof params.value === 'undefined' + ? fallbackParams + : { value: params.value }; + + return { + commandId, + params: commandParams && typeof commandParams === 'object' ? commandParams : undefined, + }; +} + +function getParagraphMenuType(target: IDocBlockMenuTarget | null | undefined, emptyMode: boolean): string { + if (target?.kind === 'table') { + return DOC_TABLE_BLOCK_MENU_ID; + } + + return emptyMode ? EMPTY_PARAGRAPH_MENU_ID : ContextMenuPosition.PARAGRAPH; +} + +export function shouldShowParagraphSettingMenu(target: IDocBlockMenuTarget | null | undefined): boolean { + return !target || target.kind === 'paragraph'; +} + export const ParagraphMenu = ({ popup }: { popup: IPopup }) => { const [visible, setVisible] = useState(false); + const [emptyMode, setEmptyMode] = useState(false); + const [dropRect, setDropRect] = useState<{ left: number; right: number; top: number; bottom: number } | null>(null); + const [menuDirection, setMenuDirection] = useState<'left' | 'right'>('left'); const contentRef = useRef(null); + const targetRangeRef = useRef(null); + const dragTargetOffsetRef = useRef(null); + const dragRangeRef = useRef<{ startOffset: number; endOffset: number } | null>(null); + const isDraggingRef = useRef(false); const commandService = useDependency(ICommandService); + const docSelectionManagerService = useDependency(DocSelectionManagerService); + const docClipboardService = useDependency(IDocClipboardService); + const docContentInsertService = useDependency(DocContentInsertService); + const clipboardInterfaceService = useDependency(IClipboardInterfaceService); const layoutService = useDependency(ILayoutService); + const componentManager = useDependency(ComponentManager); const anchorRef = useRef(null); const isMouseOver = useRef(false); + const hideTimerRef = useRef(null); const renderManagerService = useDependency(IRenderManagerService); const univerInstanceService = useDependency(IUniverInstanceService); const renderUnit = renderManagerService.getRenderById(popup.unitId); const doc = univerInstanceService.getUnit(popup.unitId, UniverInstanceType.UNIVER_DOC); const docParagraphMenuService = renderUnit?.with(DocParagraphMenuService); const docEventManagerService = renderUnit?.with(DocEventManagerService); + const activeTarget = useObservable(docParagraphMenuService?.activeTarget$); const paragraph = useObservable(docEventManagerService?.hoverParagraph$); const paragraphLeft = useObservable(docEventManagerService?.hoverParagraphLeft$); - const startIndex = (paragraph ?? paragraphLeft)?.startIndex; - const paragraphObj = useMemo(() => doc?.getBody()?.paragraphs?.find((p) => p.startIndex === startIndex), [doc, paragraph]); + const currentActiveTarget = activeTarget ?? docParagraphMenuService?.activeTarget; + const activeParagraphBound = currentActiveTarget?.paragraph ?? docParagraphMenuService?.activeParagraph ?? paragraph ?? paragraphLeft; + const startIndex = activeParagraphBound?.startIndex; + const dataStream = doc?.getBody()?.dataStream ?? ''; + const paragraphObj = useMemo(() => doc?.getBody()?.paragraphs?.find((p) => p.startIndex === startIndex), [doc, startIndex]); + const isEmptyParagraph = currentActiveTarget?.emptyMode ?? isEmptyParagraphMenuTarget(dataStream, activeParagraphBound); const namedStyleType = paragraphObj?.paragraphStyle?.namedStyleType; + const activeHeadingCommandId = getParagraphMenuActiveHeadingCommandId(namedStyleType); + const hiddenHeadingCommandIds = useMemo(() => getParagraphMenuHiddenHeadingCommandIds(namedStyleType), [namedStyleType]); + const hiddenItemIds = useMemo(() => { + if (!shouldShowParagraphSettingMenu(currentActiveTarget)) { + return [...hiddenHeadingCommandIds, DocParagraphSettingPanelOperation.id]; + } + + return hiddenHeadingCommandIds; + }, [currentActiveTarget, hiddenHeadingCommandIds]); const icon = HEADING_ICON_MAP[namedStyleType ?? NamedStyleType.NORMAL_TEXT]; + const targetIconKey = currentActiveTarget?.icon ?? icon.key; + const TargetIcon = componentManager.get(targetIconKey) ?? icon.component; const anchorRect$ = useMemo(() => new BehaviorSubject({ left: 0, right: 0, @@ -53,84 +224,272 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => { bottom: 0, }), []); + const updateAnchorRect = () => { + const boundingRect = anchorRef.current?.getBoundingClientRect(); + const left = (boundingRect?.left ?? 0) - 4; + setMenuDirection(getParagraphMenuPopupDirection(left)); + anchorRect$.next({ + left, + right: boundingRect?.right ?? 0, + top: boundingRect?.top ?? 0, + bottom: boundingRect?.bottom ?? 0, + }); + }; + const handleHideMenu = () => { setVisible(false); - docParagraphMenuService?.hideParagraphMenu(true); + targetRangeRef.current = null; }; + const clearHideTimer = () => { + if (hideTimerRef.current != null) { + window.clearTimeout(hideTimerRef.current); + hideTimerRef.current = null; + } + }; + + const scheduleHideMenu = () => { + clearHideTimer(); + hideTimerRef.current = window.setTimeout(() => { + if (!isMouseOver.current && !isDraggingRef.current) { + handleHideMenu(); + } + }, 180); + }; + + const handleOpenMenu = () => { + clearHideTimer(); + const latestTarget = docParagraphMenuService?.activeTarget ?? activeTarget; + const targetRange = latestTarget + ? { + ...latestTarget.menuRange, + segmentId: activeParagraphBound?.segmentId, + } + : getParagraphMenuTargetRange(activeParagraphBound); + targetRangeRef.current = targetRange; + updateAnchorRect(); + setEmptyMode(isEmptyParagraph); + setVisible(true); + }; + + useEffect(() => () => clearHideTimer(), []); + return ( <>
{ popup.onPointerEnter?.(e); isMouseOver.current = true; - const boundingRect = anchorRef.current?.getBoundingClientRect(); - anchorRect$.next({ - left: (boundingRect?.left ?? 0) - 4, - right: boundingRect?.right ?? 0, - top: boundingRect?.top ?? 0, - bottom: boundingRect?.bottom ?? 0, - }); + handleOpenMenu(); }} onMouseLeave={() => { isMouseOver.current = false; + scheduleHideMenu(); }} - onClick={() => { - setVisible(true); - docParagraphMenuService?.setParagraphMenuActive(true); + onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + isMouseOver.current = true; + handleOpenMenu(); }} > - - + {currentActiveTarget?.draggable && ( + + )}
{visible && (
{ popup.onPointerEnter?.(e); isMouseOver.current = true; + clearHideTimer(); }} onMouseLeave={() => { isMouseOver.current = false; + scheduleHideMenu(); }} > { - const { label: id, commandId, value } = params; + menuType={getParagraphMenuType(currentActiveTarget, emptyMode)} + activeItemIds={[activeHeadingCommandId]} + hiddenItemIds={hiddenItemIds} + onOptionSelect={async (params) => { + const targetRange = targetRangeRef.current ?? getParagraphMenuTargetRange(activeParagraphBound); + const { commandId, params: commandParams } = getParagraphMenuCommand(params, targetRange); + const latestTarget = docParagraphMenuService?.activeTarget ?? activeTarget; - if (commandService) { - commandService.executeCommand(commandId ?? id as string, { value }); + if (commandId && shouldUseInsertBelowRange(commandId, params) && latestTarget?.moveRange) { + docContentInsertService.setInsertRange({ + unitId: popup.unitId, + startOffset: latestTarget.moveRange.endOffset, + endOffset: latestTarget.moveRange.endOffset, + segmentId: targetRange?.segmentId ?? '', + }); + } + + if (latestTarget?.kind === 'table' && commandId && targetRange) { + const tableRange = { + ...targetRange, + segmentId: targetRange.segmentId ?? '', + collapsed: false, + }; + const afterTableRange = { + startOffset: latestTarget.moveRange.endOffset, + endOffset: latestTarget.moveRange.endOffset, + collapsed: true, + segmentId: targetRange.segmentId ?? '', + }; + + if (commandId === DocCopyCommand.id || commandId === DocCopyCommand.name) { + await docClipboardService.copy(SliceBodyType.copy, [tableRange]); + layoutService.focus(); + handleHideMenu(); + return; + } + + if (commandId === DocPasteCommand.id) { + docSelectionManagerService.replaceTextRanges([afterTableRange], false); + const clipboardItems = await clipboardInterfaceService.read(); + await docClipboardService.paste(clipboardItems); + layoutService.focus(); + handleHideMenu(); + return; + } + + if (commandId === DocTableDeleteTableCommand.id) { + docSelectionManagerService.replaceTextRanges([tableRange], false); + } else if (params.id === INSERT_BELLOW_MENU_ID || commandId !== INSERT_BELLOW_MENU_ID) { + docSelectionManagerService.replaceTextRanges([afterTableRange], false); + } + } + + if (commandService && commandId) { + const blockRangeParams = latestTarget?.kind === 'blockRange' && latestTarget.blockRange && commandParams && typeof commandParams === 'object' + ? { + ...commandParams, + unitId: popup.unitId, + blockId: latestTarget.blockRange.blockId, + } + : commandParams; + commandService.executeCommand(commandId, blockRangeParams); } layoutService.focus(); @@ -140,6 +499,62 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
)} + {dropRect && ( +
+ )} ); }; + +export function shouldUseInsertBelowRange(commandId: string, params: IValueOption): boolean { + if (params.id === INSERT_BELLOW_MENU_ID) { + return true; + } + + const normalized = commandId.toLowerCase(); + + if (normalized.includes('insert') && (normalized.includes('below') || normalized.includes('bellow'))) { + return true; + } + + if (normalized.includes('insert') && normalized.includes('image')) { + return true; + } + + return normalized === 'doc.command.create-table' || normalized === 'doc.operation.create-table'; +} + +function DragHandleDotsIcon() { + return ( +