Skip to content

Commit 37238bf

Browse files
authored
fix: improve Docs table menus and comment interactions (#7871)
1 parent f75d89e commit 37238bf

10 files changed

Lines changed: 596 additions & 59 deletions

File tree

‎packages/docs-ui/src/controllers/render-controllers/__tests__/doc-paragraph-placeholder.render-controller.spec.ts‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
*/
1616

1717
import type { IDocumentBody, IParagraph } from '@univerjs/core';
18-
import type { IDocumentSkeletonLine, IDocumentSkeletonPage } from '@univerjs/engine-render';
18+
import type { IDocumentSkeletonLine, IDocumentSkeletonPage, UniverRenderingContext } from '@univerjs/engine-render';
1919
import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DocumentFlavor, NamedStyleType } from '@univerjs/core';
20-
import { DocumentSkeletonPageType, GlyphType, LineType, setDocsTableRenderViewportProvider } from '@univerjs/engine-render';
20+
import {
21+
DocumentSkeletonPageType,
22+
GlyphType,
23+
LineType,
24+
setDocsTableRenderViewportProvider,
25+
} from '@univerjs/engine-render';
2126
import { afterEach, describe, expect, it, vi } from 'vitest';
2227
import {
2328
drawParagraphPlaceholders,
@@ -359,12 +364,46 @@ describe('doc paragraph placeholder render controller', () => {
359364
}]);
360365
});
361366

367+
it('paints hints at half the inherited opacity without fading subsequent document content', () => {
368+
const alphaStack: number[] = [];
369+
const paintedAlphas: number[] = [];
370+
const context = {
371+
globalAlpha: 0.8,
372+
save() {
373+
alphaStack.push(this.globalAlpha);
374+
},
375+
restore() {
376+
this.globalAlpha = alphaStack.pop()!;
377+
},
378+
beginPath: vi.fn(),
379+
clip: vi.fn(),
380+
closePath: vi.fn(),
381+
rectByPrecision: vi.fn(),
382+
fillText() {
383+
paintedAlphas.push(this.globalAlpha);
384+
},
385+
};
386+
drawParagraphPlaceholders(context as unknown as UniverRenderingContext, [{
387+
fontFamily: 'Arial',
388+
fontSize: 16,
389+
fontWeight: 'normal',
390+
maxWidth: 200,
391+
text: 'Hint',
392+
x: 20,
393+
y: 40,
394+
}]);
395+
expect(paintedAlphas).toEqual([0.4]);
396+
expect(context.globalAlpha).toBe(0.8);
397+
expect(alphaStack).toEqual([]);
398+
});
399+
362400
it('clips long placeholder text instead of scaling its glyphs to maxWidth', () => {
363401
const ctx = {
364402
beginPath: vi.fn(),
365403
clip: vi.fn(),
366404
closePath: vi.fn(),
367405
fillText: vi.fn(),
406+
globalAlpha: 1,
368407
rectByPrecision: vi.fn(),
369408
restore: vi.fn(),
370409
save: vi.fn(),

‎packages/docs-ui/src/controllers/render-controllers/doc-paragraph-placeholder.render-controller.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ export function drawParagraphPlaceholders(
422422
): void {
423423
ctx.save();
424424
ctx.fillStyle = color;
425+
ctx.globalAlpha *= 0.5;
425426
ctx.textBaseline = 'alphabetic';
426427

427428
for (const placeholder of placeholders) {

‎packages/docs-ui/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export { scrollDocsTableLikeCustomBlockLive } from './embed-docs-custom-block-sc
175175
export type { IDocsTableLikeCustomBlockScrollOptions } from './embed-docs-custom-block-scroll';
176176
export { resolveDocsCustomBlockRenderViewport } from './embed-host-anchor';
177177
export type { IDocsCustomBlockLayoutViewport, IDocsCustomBlockRenderViewportParams } from './embed-host-anchor';
178+
export { ParagraphSettingMenuFactory } from './menu/context-menu';
178179
export {
179180
AlignMenuItemFactory,
180181
BackgroundColorSelectorMenuItemFactory,

‎packages/docs-ui/src/services/__tests__/doc-paragraph-menu.service.spec.ts‎

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,190 @@
1414
* limitations under the License.
1515
*/
1616

17+
import type { DocumentDataModel, IDocumentData } from '@univerjs/core';
18+
import type { RenderUnit } from '@univerjs/engine-render';
1719
import type { IMutiPageParagraphBound, ITableBound } from '../doc-event-manager.service';
1820
import {
1921
BlockType,
2022
DataStreamTreeTokenType,
2123
DOC_RANGE_TYPE,
2224
DocumentBlockRangeType,
2325
DocumentBlockType,
26+
DocumentFlavor,
27+
IPermissionService,
28+
LocaleService,
29+
LocaleType,
2430
PresetListType,
31+
Univer,
32+
UniverInstanceType,
2533
} from '@univerjs/core';
26-
import { createDocumentPermissionPoint, getDocumentParagraphPermissionObjectId } from '@univerjs/docs';
27-
import { DocumentEditArea } from '@univerjs/engine-render';
34+
import {
35+
createDocumentPermissionPoint,
36+
DocSkeletonManagerService,
37+
getDocumentParagraphPermissionObjectId,
38+
setDocumentPermissionValue,
39+
UniverDocsPlugin,
40+
} from '@univerjs/docs';
41+
import {
42+
CanvasColorService,
43+
DocumentEditArea,
44+
Documents,
45+
ICanvasColorService,
46+
IRenderManagerService,
47+
RenderManagerService,
48+
Viewport,
49+
} from '@univerjs/engine-render';
2850
import { UnitAction } from '@univerjs/protocol';
51+
import {
52+
CanvasPopupService,
53+
ContextMenuService,
54+
DesktopLayoutService,
55+
ICanvasPopupService,
56+
IContextMenuService,
57+
ILayoutService,
58+
} from '@univerjs/ui';
2959
import { BehaviorSubject, Subject } from 'rxjs';
30-
import { describe, expect, it, vi } from 'vitest';
60+
import { describe, expect, it, onTestFinished, vi } from 'vitest';
61+
import { buildMoveDocBlockActions } from '../../commands/commands/doc-block-move.command';
62+
import { genTableSource } from '../../commands/commands/table/table';
63+
import enUS from '../../locale/en-US';
3164
import {
3265
DOC_PARAGRAPH_MENU_COMPONENT_KEY,
3366
DOC_TABLE_BLOCK_MENU_COMPONENT_KEY,
3467
} from '../../views/paragraph-menu/component-keys';
3568
import {
69+
DocEventManagerService,
3670
getPreferredParagraphBoundsInRange,
3771
getTableBlockMenuHoverRect,
3872
getTableHorizontalViewportGeometry,
3973
} from '../doc-event-manager.service';
4074
import { DocLayoutInteractionService } from '../doc-layout-interaction.service';
4175
import { DocParagraphMenuService } from '../doc-paragraph-menu.service';
76+
import { DocCanvasPopManagerService } from '../doc-popup-manager.service';
77+
import { DocFloatMenuService } from '../float-menu.service';
78+
import { DocSelectionRenderService } from '../selection/doc-selection-render.service';
79+
80+
describe('focused table menus with real render services', () => {
81+
function createFocusedMenuTestBed() {
82+
const context = new Proxy({
83+
font: '',
84+
measureText: (text: string) => ({ width: text.length * 8, actualBoundingBoxAscent: 8, actualBoundingBoxDescent: 2 }),
85+
}, { get: (target, key) => key in target ? Reflect.get(target, key) : () => {} });
86+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context as never);
87+
const univer = new Univer({ locale: LocaleType.EN_US, locales: { [LocaleType.EN_US]: enUS } });
88+
onTestFinished(() => {
89+
univer.dispose();
90+
vi.restoreAllMocks();
91+
});
92+
univer.registerPlugin(UniverDocsPlugin);
93+
const injector = univer.__getInjector();
94+
injector.get(LocaleService).setDirection('ltr');
95+
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
96+
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
97+
injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]);
98+
injector.add([IContextMenuService, { useClass: ContextMenuService }]);
99+
injector.add([ILayoutService, { useClass: DesktopLayoutService }]);
100+
injector.add([DocCanvasPopManagerService]);
101+
const table = genTableSource(1, 1, 200);
102+
const tableStream = `${DataStreamTreeTokenType.TABLE_START}${DataStreamTreeTokenType.TABLE_ROW_START}${DataStreamTreeTokenType.TABLE_CELL_START}Cell\r\n${DataStreamTreeTokenType.TABLE_CELL_END}${DataStreamTreeTokenType.TABLE_ROW_END}${DataStreamTreeTokenType.TABLE_END}`;
103+
const model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, {
104+
id: 'focused-table-doc',
105+
body: {
106+
dataStream: `${tableStream}\rBody\r\n`,
107+
paragraphs: [
108+
{ startIndex: 7, paragraphId: 'cell' },
109+
{ startIndex: tableStream.length, paragraphId: 'after-table' },
110+
{ startIndex: tableStream.length + 5, paragraphId: 'body' },
111+
],
112+
tables: [{ tableId: table.tableId, startIndex: 0, endIndex: tableStream.length }],
113+
},
114+
tableSource: { [table.tableId]: table },
115+
documentStyle: {
116+
documentFlavor: DocumentFlavor.TRADITIONAL,
117+
pageSize: { width: 300, height: 400 },
118+
marginTop: 20,
119+
marginBottom: 20,
120+
marginLeft: 20,
121+
marginRight: 20,
122+
},
123+
});
124+
const render = injector.get(IRenderManagerService).createRender(model.getUnitId()) as RenderUnit;
125+
render.deactivate();
126+
render.engine.resizeBySize(300, 400);
127+
const canvas = render.engine.getCanvasElement();
128+
canvas.style.width = '300px';
129+
vi.spyOn(canvas, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 300, 400));
130+
const viewport = new Viewport('viewMain', render.scene, { left: 0, top: 0, width: 300, height: 400 });
131+
onTestFinished(() => viewport.dispose());
132+
render.addRenderDependencies([[DocSkeletonManagerService], [DocLayoutInteractionService]]);
133+
const skeleton = render.with(DocSkeletonManagerService).getSkeleton();
134+
const documents = new Documents('focused-table-documents', skeleton);
135+
render.mainComponent = documents;
136+
render.scene.addObject(documents);
137+
render.addRenderDependencies([
138+
[DocSelectionRenderService],
139+
[DocEventManagerService],
140+
[DocFloatMenuService],
141+
[DocParagraphMenuService],
142+
]);
143+
const service = render.with(DocParagraphMenuService);
144+
const bound = render.with(DocEventManagerService).tableBounds.get(table.tableId)!;
145+
return { injector, service, bound, render, model };
146+
}
147+
148+
it('keeps a focused table trigger independent of paragraph hover and closes each popup independently', () => {
149+
const { injector, service, bound, render, model } = createFocusedMenuTestBed();
150+
const popups = injector.get(ICanvasPopupService);
151+
service.showTableMenu(bound, true);
152+
expect(service.focusedTableTarget?.table?.tableId).toBe(bound.tableId);
153+
expect(service.focusedTableTarget?.moveRange.endOffset)
154+
.toBe(service.focusedTableTarget!.table!.endIndex + 1);
155+
const sourceRange = service.focusedTableTarget!.moveRange;
156+
const snapshot = model.getSnapshot();
157+
const moved = buildMoveDocBlockActions({
158+
documentData: snapshot,
159+
sourceRange,
160+
targetOffset: snapshot.body!.dataStream.length - 1,
161+
});
162+
expect(moved.nextDocumentData.body!.dataStream.endsWith('\r\n')).toBe(true);
163+
const restored = buildMoveDocBlockActions({
164+
documentData: moved.nextDocumentData,
165+
sourceRange: moved.movedRange,
166+
targetOffset: sourceRange.startOffset,
167+
});
168+
expect(restored.nextDocumentData.body).toEqual(snapshot.body);
169+
service.showParagraphMenu(createParagraphBound({ paragraphStart: 3, paragraphEnd: 7, startIndex: 7 }));
170+
expect(service.activeTarget?.kind).toBe(DocumentBlockType.PARAGRAPH);
171+
expect(popups.popups).toHaveLength(2);
172+
service.setParagraphMenuActive(true, true);
173+
service.hideParagraphMenu(true);
174+
expect(popups.popups).toHaveLength(1);
175+
expect(render.with(DocLayoutInteractionService).isActive).toBe(true);
176+
const dismiss = vi.fn();
177+
const subscription = service.focusedTableMenuDismiss$.subscribe(dismiss);
178+
onTestFinished(() => subscription.unsubscribe());
179+
popups.popups[0][1].onClickOutside?.(new PointerEvent('pointerdown'));
180+
expect(dismiss).toHaveBeenCalledOnce();
181+
expect(service.focusedTableTarget?.table?.tableId).toBe(bound.tableId);
182+
service.hideFocusedTableMenu();
183+
expect(popups.popups).toHaveLength(0);
184+
expect(render.with(DocLayoutInteractionService).isActive).toBe(false);
185+
});
186+
187+
it('releases focused popups on permission loss and service disposal', () => {
188+
const { injector, service, bound } = createFocusedMenuTestBed();
189+
const popups = injector.get(ICanvasPopupService);
190+
service.showTableMenu(bound, true);
191+
setDocumentPermissionValue(injector.get(IPermissionService), 'focused-table-doc', 'focused-table-doc', UnitAction.Edit, false);
192+
expect(service.focusedTableTarget).toBeNull();
193+
expect(popups.popups).toHaveLength(0);
194+
setDocumentPermissionValue(injector.get(IPermissionService), 'focused-table-doc', 'focused-table-doc', UnitAction.Edit, true);
195+
service.showTableMenu(bound, true);
196+
expect(popups.popups).toHaveLength(1);
197+
service.dispose();
198+
expect(popups.popups).toHaveLength(0);
199+
});
200+
});
42201

43202
describe('DocParagraphMenuService', () => {
44203
it('shows the paragraph menu for empty paragraphs', () => {

0 commit comments

Comments
 (0)