mirror of
https://github.com/dream-num/univer.git
synced 2026-09-21 13:50:19 +08:00
feat(docs): restore document polish changes (#7055)
This commit is contained in:
@@ -21,7 +21,7 @@ import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import * as paragraphMenu from '..';
|
||||
import { createParagraphMenuHoverOpenScheduler, getParagraphFormattingRange, getParagraphMenuActiveHeadingCommandId, getParagraphMenuCommand, getParagraphMenuCommandTargetRange, getParagraphMenuHiddenHeadingCommandIds, getParagraphMenuHiddenItemIds, getParagraphMenuIconSizeClass, getParagraphMenuPopupDirection, getParagraphMenuTargetRange, isEmptyParagraphMenuTarget, PARAGRAPH_MENU_HOVER_OPEN_DELAY, shouldShowParagraphSettingMenu, shouldUseInsertBelowRange } from '..';
|
||||
import { createParagraphMenuHoverOpenScheduler, getParagraphFormattingRange, getParagraphMenuActiveHeadingCommandId, getParagraphMenuCommand, getParagraphMenuCommandTargetRange, getParagraphMenuHiddenHeadingCommandIds, getParagraphMenuHiddenItemIds, getParagraphMenuIconSizeClass, getParagraphMenuPopupDirection, getParagraphMenuResolvedCommand, getParagraphMenuTargetRange, isEmptyParagraphMenuTarget, PARAGRAPH_MENU_HOVER_OPEN_DELAY, shouldShowParagraphSettingMenu, shouldUseInsertBelowRange } from '..';
|
||||
import { HorizontalLineCommand } from '../../../commands/commands/doc-horizontal-line.command';
|
||||
import { SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand } from '../../../commands/commands/inline-format.command';
|
||||
import { BulletListCommand, InsertBulletListBellowCommand, OrderListCommand } from '../../../commands/commands/list.command';
|
||||
@@ -168,20 +168,32 @@ describe('ParagraphMenu', () => {
|
||||
});
|
||||
|
||||
it('reuses the existing text color selector logic for the colors header action with an A icon', () => {
|
||||
const registeredIcons = new Map<string, React.ComponentType<{ className?: string }>>();
|
||||
const componentManager = {
|
||||
get: (key: string) => registeredIcons.get(key),
|
||||
register: (key: string, component: React.ComponentType<{ className?: string }>) => {
|
||||
registeredIcons.set(key, component);
|
||||
},
|
||||
};
|
||||
const accessor = {
|
||||
get: () => ({
|
||||
get: () => undefined,
|
||||
register: () => undefined,
|
||||
}),
|
||||
get: () => componentManager,
|
||||
} as never;
|
||||
|
||||
const menuItem = ParagraphMenuTextColorHeaderActionMenuItemFactory(accessor);
|
||||
const Icon = registeredIcons.get(menuItem.icon as string);
|
||||
|
||||
expect(menuItem.type).toBe(MenuItemType.BUTTON_SELECTOR);
|
||||
expect(menuItem.id).toBe(SetInlineFormatTextColorCommand.id);
|
||||
expect(menuItem.icon).toBe('HeaderTextColorIcon');
|
||||
expect(menuItem.tooltip).toBeUndefined();
|
||||
expect(Array.isArray((menuItem as any).selections)).toBe(true);
|
||||
expect(Icon).toBeDefined();
|
||||
|
||||
const markup = renderToStaticMarkup(React.createElement(Icon!, { className: 'header-color-icon' }));
|
||||
|
||||
expect(markup).toContain('>A<');
|
||||
expect(markup).toContain('width="1em"');
|
||||
expect(markup).toContain('height="1em"');
|
||||
});
|
||||
|
||||
it('reuses the existing background color selector logic for the colors header action with the bucket icon', () => {
|
||||
@@ -424,6 +436,24 @@ describe('ParagraphMenu', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves custom color values when resolving paragraph menu commands', () => {
|
||||
expect(getParagraphMenuResolvedCommand({
|
||||
id: SetInlineFormatTextColorCommand.id,
|
||||
value: '#ff5500',
|
||||
}, null)).toEqual({
|
||||
commandId: SetInlineFormatTextColorCommand.id,
|
||||
params: { value: '#ff5500' },
|
||||
});
|
||||
|
||||
expect(getParagraphMenuResolvedCommand({
|
||||
id: SetInlineFormatTextBackgroundColorCommand.id,
|
||||
value: 'rgba(255, 140, 81, 0.3)',
|
||||
}, null)).toEqual({
|
||||
commandId: SetInlineFormatTextBackgroundColorCommand.id,
|
||||
params: { value: 'rgba(255, 140, 81, 0.3)' },
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the hovered paragraph range to current-paragraph menu commands', () => {
|
||||
const targetRange = { startOffset: 3, endOffset: 3, collapsed: true };
|
||||
|
||||
|
||||
@@ -270,6 +270,20 @@ export function getParagraphMenuCommand(params: IValueOption, targetRange?: ITex
|
||||
};
|
||||
}
|
||||
|
||||
export function getParagraphMenuResolvedCommand(option: IValueOption, targetRange?: ITextRangeWithStyle | null): { commandId?: string; params?: object } {
|
||||
const commandId = option.commandId ?? option.id ?? (typeof option.label === 'string' ? option.label : undefined);
|
||||
|
||||
if (!commandId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getParagraphMenuCommand({
|
||||
...option,
|
||||
commandId,
|
||||
id: option.id ?? commandId,
|
||||
}, targetRange);
|
||||
}
|
||||
|
||||
function getParagraphMenuType(target: IDocBlockMenuTarget | null | undefined, emptyMode: boolean): string {
|
||||
if (target?.kind === 'table') {
|
||||
return DOC_TABLE_BLOCK_MENU_ID;
|
||||
@@ -681,12 +695,8 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
docSelectionManagerService.replaceTextRanges([range], false);
|
||||
};
|
||||
|
||||
const executeResolvedCommand = (commandId: string, params?: Record<string, unknown>, targetRange?: ITextRangeWithStyle | null) => {
|
||||
const resolved = getParagraphMenuCommand({
|
||||
commandId,
|
||||
id: commandId,
|
||||
params,
|
||||
}, targetRange);
|
||||
const executeResolvedCommand = (option: IValueOption, targetRange?: ITextRangeWithStyle | null) => {
|
||||
const resolved = getParagraphMenuResolvedCommand(option, targetRange);
|
||||
|
||||
if (!resolved.commandId) {
|
||||
return false;
|
||||
@@ -812,7 +822,11 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
) {
|
||||
const nextRange = await unwrapActiveBlockRange();
|
||||
if (commandId !== NormalTextHeadingCommand.id) {
|
||||
await executeResolvedCommand(commandId, commandParams as Record<string, unknown> | undefined, nextRange);
|
||||
await executeResolvedCommand({
|
||||
...option,
|
||||
commandId,
|
||||
params: commandParams as Record<string, unknown> | undefined,
|
||||
}, nextRange);
|
||||
}
|
||||
layoutService.focus();
|
||||
handleHideMenu();
|
||||
@@ -827,7 +841,11 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
if (blockRange) {
|
||||
replaceSelection(blockRange);
|
||||
}
|
||||
await executeResolvedCommand(commandId, commandParams as Record<string, unknown> | undefined, blockRange);
|
||||
await executeResolvedCommand({
|
||||
...option,
|
||||
commandId,
|
||||
params: commandParams as Record<string, unknown> | undefined,
|
||||
}, blockRange);
|
||||
layoutService.focus();
|
||||
handleHideMenu();
|
||||
return;
|
||||
@@ -838,7 +856,9 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
if (targetRange) {
|
||||
replaceSelection(targetRange);
|
||||
}
|
||||
await executeResolvedCommand(NormalTextHeadingCommand.id, undefined, targetRange);
|
||||
await executeResolvedCommand({
|
||||
id: NormalTextHeadingCommand.id,
|
||||
}, targetRange);
|
||||
layoutService.focus();
|
||||
handleHideMenu();
|
||||
return;
|
||||
@@ -855,11 +875,11 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
replaceSelection(getParagraphMenuCommandTargetRange(commandId, targetRange, formattingRange));
|
||||
}
|
||||
|
||||
await executeResolvedCommand(
|
||||
await executeResolvedCommand({
|
||||
...option,
|
||||
commandId,
|
||||
commandParams as Record<string, unknown> | undefined,
|
||||
getParagraphMenuCommandTargetRange(commandId, targetRange, formattingRange)
|
||||
);
|
||||
params: commandParams as Record<string, unknown> | undefined,
|
||||
}, getParagraphMenuCommandTargetRange(commandId, targetRange, formattingRange));
|
||||
layoutService.focus();
|
||||
handleHideMenu();
|
||||
};
|
||||
|
||||
+1
-11
@@ -16,7 +16,7 @@
|
||||
|
||||
import { DocumentFlavor } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getRuntimeDocZoomRatio, shouldHandleDocWheelZoom } from '../zoom.render-controller';
|
||||
import { shouldHandleDocWheelZoom } from '../zoom.render-controller';
|
||||
|
||||
describe('DocZoomRenderController', () => {
|
||||
it('handles wheel zoom intent for focused modern docs', () => {
|
||||
@@ -28,14 +28,4 @@ describe('DocZoomRenderController', () => {
|
||||
expect(shouldHandleDocWheelZoom({ ctrlKey: false, metaKey: false }, true, DocumentFlavor.TRADITIONAL)).toBe(false);
|
||||
expect(shouldHandleDocWheelZoom({ ctrlKey: true, metaKey: false }, false, DocumentFlavor.TRADITIONAL)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses a larger runtime zoom default for modern docs without persisting it', () => {
|
||||
expect(getRuntimeDocZoomRatio(undefined, DocumentFlavor.MODERN)).toBe(1.2);
|
||||
expect(getRuntimeDocZoomRatio(undefined, DocumentFlavor.TRADITIONAL)).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps explicit document zoom settings ahead of runtime defaults', () => {
|
||||
expect(getRuntimeDocZoomRatio(0.85, DocumentFlavor.MODERN)).toBe(0.85);
|
||||
expect(getRuntimeDocZoomRatio(1.5, DocumentFlavor.TRADITIONAL)).toBe(1.5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import type { DocumentDataModel, ICommandInfo, Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule, IWheelEvent } from '@univerjs/engine-render';
|
||||
|
||||
import type { IDocPageSetupCommandParams } from '../../commands/commands/doc-page-setup.command';
|
||||
import type { ISetDocZoomRatioOperationParams } from '../../commands/operations/set-doc-zoom-ratio.operation';
|
||||
import {
|
||||
Disposable,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
@@ -37,6 +39,7 @@ import { SetDocZoomRatioCommand } from '../../commands/commands/set-doc-zoom-rat
|
||||
import { SwitchDocModeCommand } from '../../commands/commands/switch-doc-mode.command';
|
||||
import { SetDocZoomRatioOperation } from '../../commands/operations/set-doc-zoom-ratio.operation';
|
||||
import { DocPageLayoutService } from '../../services/doc-page-layout.service';
|
||||
import { DEFAULT_MODERN_DOC_ZOOM_RATIO, getDocEffectiveZoomRatio } from '../../services/doc-zoom';
|
||||
import { IEditorService } from '../../services/editor/editor-manager.service';
|
||||
|
||||
export function shouldHandleDocWheelZoom(
|
||||
@@ -47,14 +50,6 @@ export function shouldHandleDocWheelZoom(
|
||||
return focusingDoc && (event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
export function getDefaultDocZoomRatio(documentFlavor?: DocumentFlavor): number {
|
||||
return documentFlavor === DocumentFlavor.MODERN ? 1.2 : 1;
|
||||
}
|
||||
|
||||
export function getRuntimeDocZoomRatio(savedZoomRatio?: number, documentFlavor?: DocumentFlavor): number {
|
||||
return savedZoomRatio ?? getDefaultDocZoomRatio(documentFlavor);
|
||||
}
|
||||
|
||||
export class DocZoomRenderController extends Disposable implements IRenderModule {
|
||||
private _isSheetEditor = false;
|
||||
private _initTimer: number;
|
||||
@@ -80,12 +75,10 @@ export class DocZoomRenderController extends Disposable implements IRenderModule
|
||||
const sheetRenderer = currentSheet && this._renderManagerService.getRenderById(currentSheet.getUnitId());
|
||||
// TODO: do not use setTimeout.
|
||||
this._initTimer = window.setTimeout(() => {
|
||||
const documentModel = this._univerInstanceService.getCurrentUniverDocInstance();
|
||||
const zoomRatio = sheetRenderer && this._isSheetEditor
|
||||
? sheetRenderer.scene.scaleX
|
||||
: documentModel
|
||||
? this._getRuntimeZoomRatio(documentModel)
|
||||
: 1;
|
||||
: getDocEffectiveZoomRatio(this._context.unit);
|
||||
|
||||
this.updateViewZoom(zoomRatio, true);
|
||||
}, 20);
|
||||
|
||||
@@ -111,9 +104,7 @@ export class DocZoomRenderController extends Disposable implements IRenderModule
|
||||
this._updateTimer = window.setTimeout(() => {
|
||||
const currentSheet = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const sheetRenderer = currentSheet && this._renderManagerService.getRenderById(currentSheet.getUnitId());
|
||||
const zoomRatio = !this._isSheetEditor
|
||||
? this._getRuntimeZoomRatio(documentModel)
|
||||
: sheetRenderer?.scene.scaleX || 1;
|
||||
const zoomRatio = !this._isSheetEditor ? getDocEffectiveZoomRatio(documentModel) : sheetRenderer?.scene.scaleX || 1;
|
||||
|
||||
this.updateViewZoom(zoomRatio, false);
|
||||
});
|
||||
@@ -121,35 +112,29 @@ export class DocZoomRenderController extends Disposable implements IRenderModule
|
||||
}
|
||||
|
||||
private _initCommandExecutedListener() {
|
||||
const updateCommandList = [SetDocZoomRatioOperation.id, SwitchDocModeCommand.id, DocPageSetupCommand.id];
|
||||
const updateCommandList = [SetDocZoomRatioOperation.id];
|
||||
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (!updateCommandList.includes(command.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = (command.params as { unitId?: string; documentId?: string } | undefined)?.unitId
|
||||
?? (command.params as { unitId?: string; documentId?: string } | undefined)?.documentId
|
||||
?? this._context.unitId;
|
||||
|
||||
if (unitId !== this._context.unitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const documentModel = this._context.unit;
|
||||
|
||||
if (command.id === SetDocZoomRatioOperation.id) {
|
||||
const zoomRatio = documentModel.zoomRatio || 1;
|
||||
if (updateCommandList.includes(command.id) && (command.params as ISetDocZoomRatioOperationParams).unitId === this._context.unitId) {
|
||||
const documentModel = this._context.unit;
|
||||
const zoomRatio = getDocEffectiveZoomRatio(documentModel);
|
||||
this.updateViewZoom(zoomRatio);
|
||||
return;
|
||||
}
|
||||
|
||||
if (documentModel.getSettings()?.zoomRatio != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateViewZoom(this._getRuntimeZoomRatio(documentModel));
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
const shouldResetZoom = command.id === SwitchDocModeCommand.id ||
|
||||
(command.id === DocPageSetupCommand.id && (command.params as IDocPageSetupCommandParams | undefined)?.documentFlavor === DocumentFlavor.MODERN);
|
||||
|
||||
if (shouldResetZoom) {
|
||||
this._commandService.executeCommand(SetDocZoomRatioCommand.id, {
|
||||
zoomRatio: DEFAULT_MODERN_DOC_ZOOM_RATIO,
|
||||
documentId: this._context.unitId,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateViewZoom(zoomRatio: number, needRefreshSelection = true) {
|
||||
@@ -186,7 +171,7 @@ export class DocZoomRenderController extends Disposable implements IRenderModule
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRatio = this._getRuntimeZoomRatio(documentModel);
|
||||
const currentRatio = getDocEffectiveZoomRatio(documentModel);
|
||||
const nextRatio = getNextWheelZoomRatio(currentRatio, e);
|
||||
|
||||
this._commandService.executeCommand(SetDocZoomRatioCommand.id, {
|
||||
@@ -198,11 +183,4 @@ export class DocZoomRenderController extends Disposable implements IRenderModule
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _getRuntimeZoomRatio(documentModel: DocumentDataModel): number {
|
||||
return getRuntimeDocZoomRatio(
|
||||
documentModel.getSettings()?.zoomRatio,
|
||||
documentModel.getSnapshot().documentStyle?.documentFlavor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ function HeaderTextColorIcon({ className, extend }: { className: string; extend?
|
||||
{
|
||||
className,
|
||||
viewBox: '0 0 24 24',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
fill: 'none',
|
||||
'aria-hidden': true,
|
||||
},
|
||||
@@ -246,7 +248,7 @@ export const EmptyParagraphBulletListMenuItemFactory = createEmptyParagraphButto
|
||||
export const EmptyParagraphCheckListMenuItemFactory = createEmptyParagraphButtonFactory(CheckListCommand, 'TodoListDoubleIcon', 'docs-ui.rightClick.checkList');
|
||||
export const EmptyParagraphHorizontalLineMenuItemFactory = createEmptyParagraphButtonFactory(HorizontalLineCommand, 'ReduceIcon', 'docs-ui.toolbar.horizontalLine');
|
||||
|
||||
export const CopyCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const CopyCurrentParagraphMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocCopyCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -255,7 +257,7 @@ export const CopyCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuI
|
||||
};
|
||||
};
|
||||
|
||||
export const CutCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const CutCurrentParagraphMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocCutCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -264,7 +266,7 @@ export const CutCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuIt
|
||||
};
|
||||
};
|
||||
|
||||
export const DeleteCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const DeleteCurrentParagraphMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DeleteCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -273,7 +275,7 @@ export const DeleteCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMen
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertBulletListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const InsertBulletListBellowMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertBulletListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -283,7 +285,7 @@ export const InsertBulletListBellowMenuItemFactory = (accessor: IAccessor): IMen
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertOrderListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const InsertOrderListBellowMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertOrderListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -293,7 +295,7 @@ export const InsertOrderListBellowMenuItemFactory = (accessor: IAccessor): IMenu
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertCheckListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const InsertCheckListBellowMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertCheckListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -303,7 +305,7 @@ export const InsertCheckListBellowMenuItemFactory = (accessor: IAccessor): IMenu
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertHorizontalLineBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const InsertHorizontalLineBellowMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertHorizontalLineBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -389,6 +391,8 @@ function TextColorSwatchIcon(props: { className?: string; color: string }) {
|
||||
{
|
||||
className,
|
||||
viewBox: '0 0 24 24',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
fill: 'none',
|
||||
'aria-hidden': true,
|
||||
},
|
||||
@@ -420,6 +424,8 @@ function BackgroundColorSwatchIcon(props: { className?: string; color: string })
|
||||
{
|
||||
className,
|
||||
viewBox: '0 0 24 24',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
fill: 'none',
|
||||
'aria-hidden': true,
|
||||
},
|
||||
@@ -549,7 +555,7 @@ function createHeaderActionMenuItemFactory(
|
||||
};
|
||||
}
|
||||
|
||||
export const TableBlockCopyMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const TableBlockCopyMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocCopyCommand.name,
|
||||
commandId: DocCopyCommand.id,
|
||||
@@ -559,7 +565,7 @@ export const TableBlockCopyMenuItemFactory = (accessor: IAccessor): IMenuItem =>
|
||||
};
|
||||
};
|
||||
|
||||
export const TableBlockPasteMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const TableBlockPasteMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocPasteCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -568,7 +574,7 @@ export const TableBlockPasteMenuItemFactory = (accessor: IAccessor): IMenuItem =
|
||||
};
|
||||
};
|
||||
|
||||
export const TableBlockDeleteMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
export const TableBlockDeleteMenuItemFactory = (_accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocTableDeleteTableCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
@@ -577,7 +583,7 @@ export const TableBlockDeleteMenuItemFactory = (accessor: IAccessor): IMenuItem
|
||||
};
|
||||
};
|
||||
|
||||
export function DocInsertBellowMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<string> {
|
||||
export function DocInsertBellowMenuItemFactory(_accessor: IAccessor): IMenuSelectorItem<string> {
|
||||
return {
|
||||
id: INSERT_BELLOW_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { DocumentFlavor } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DEFAULT_MODERN_DOC_ZOOM_RATIO, getDocEffectiveZoomRatio } from '../doc-zoom';
|
||||
|
||||
describe('doc zoom helpers', () => {
|
||||
it('defaults modern docs to 120 percent when no explicit zoom is stored', () => {
|
||||
const documentModel = {
|
||||
getSettings: () => undefined,
|
||||
getSnapshot: () => ({
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.MODERN,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
expect(getDocEffectiveZoomRatio(documentModel as never)).toBe(DEFAULT_MODERN_DOC_ZOOM_RATIO);
|
||||
});
|
||||
|
||||
it('keeps explicit zoom ratios and classic defaults unchanged', () => {
|
||||
expect(getDocEffectiveZoomRatio({
|
||||
getSettings: () => ({ zoomRatio: 1.35 }),
|
||||
getSnapshot: () => ({
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.MODERN,
|
||||
},
|
||||
}),
|
||||
} as never)).toBe(1.35);
|
||||
|
||||
expect(getDocEffectiveZoomRatio({
|
||||
getSettings: () => undefined,
|
||||
getSnapshot: () => ({
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
},
|
||||
}),
|
||||
} as never)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable } from '@univerjs/core';
|
||||
import { neoGetDocObject } from '../basics/component-tools';
|
||||
import { VIEWPORT_KEY } from '../basics/docs-view-key';
|
||||
import { getDocEffectiveZoomRatio } from './doc-zoom';
|
||||
|
||||
export class DocPageLayoutService extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
@@ -32,7 +33,7 @@ export class DocPageLayoutService extends Disposable implements IRenderModule {
|
||||
|
||||
const docObject = neoGetDocObject(this._context);
|
||||
const docDataModel = this._context.unit;
|
||||
const zoomRatio = docDataModel.getSettings()?.zoomRatio ?? 1;
|
||||
const zoomRatio = getDocEffectiveZoomRatio(docDataModel);
|
||||
const { document: docsComponent, scene, docBackground } = docObject;
|
||||
|
||||
const parent = scene?.getParent();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { DocumentFlavor } from '@univerjs/core';
|
||||
|
||||
const DEFAULT_DOC_ZOOM_RATIO = 1;
|
||||
export const DEFAULT_MODERN_DOC_ZOOM_RATIO = 1.2;
|
||||
|
||||
type DocZoomSource = Pick<DocumentDataModel, 'getSettings' | 'getSnapshot'>;
|
||||
|
||||
export function getDocEffectiveZoomRatio(documentModel: DocZoomSource): number {
|
||||
const zoomRatio = documentModel.getSettings()?.zoomRatio;
|
||||
if (zoomRatio != null) {
|
||||
return zoomRatio;
|
||||
}
|
||||
|
||||
const documentFlavor = documentModel.getSnapshot().documentStyle?.documentFlavor;
|
||||
return documentFlavor === DocumentFlavor.MODERN
|
||||
? DEFAULT_MODERN_DOC_ZOOM_RATIO
|
||||
: DEFAULT_DOC_ZOOM_RATIO;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { ICommandService, IUniverInstanceService, UniverInstanceType } from '@un
|
||||
import { Slider, useDependency } from '@univerjs/ui';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { SetDocZoomRatioOperation } from '../../commands/operations/set-doc-zoom-ratio.operation';
|
||||
import { getDocEffectiveZoomRatio } from '../../services/doc-zoom';
|
||||
|
||||
const ZOOM_MAP = [50, 80, 100, 130, 150, 170, 200, 400];
|
||||
const DOC_ZOOM_RANGE = [10, 400];
|
||||
@@ -29,11 +30,10 @@ export function ZoomSlider() {
|
||||
const [documentDataModel, setDocumentDataModel] = useState<DocumentDataModel | null>(null);
|
||||
const [zoom, setZoom] = useState<number>(100);
|
||||
|
||||
const getCurrentZoom = useCallback(() => {
|
||||
if (!documentDataModel) return 100;
|
||||
const getCurrentZoom = useCallback((docModel: DocumentDataModel | null = documentDataModel) => {
|
||||
if (!docModel) return 100;
|
||||
|
||||
const currentZoom = ((documentDataModel.getSettings()?.zoomRatio ?? 1) * 100);
|
||||
return Math.round(currentZoom);
|
||||
return Math.round(getDocEffectiveZoomRatio(docModel) * 100);
|
||||
}, [documentDataModel]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,7 +42,7 @@ export function ZoomSlider() {
|
||||
const subscription = currentDoc$.subscribe((doc) => {
|
||||
if (doc) {
|
||||
setDocumentDataModel(doc);
|
||||
setZoom(getCurrentZoom());
|
||||
setZoom(getCurrentZoom(doc));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { DOCS_EXTENSION_TYPE } from '../doc-extension';
|
||||
import { Documents } from '../document';
|
||||
import { setDocsTableRenderViewportProvider } from '../table-render-viewport';
|
||||
|
||||
function createGlyph(content: string, left: number, width = 16) {
|
||||
function createGlyph(content: string, left: number, width = 16, backgroundColor?: string) {
|
||||
return {
|
||||
glyphType: GlyphType.WORD,
|
||||
streamType: 'word',
|
||||
@@ -55,6 +55,11 @@ function createGlyph(content: string, left: number, width = 16) {
|
||||
fs: 12,
|
||||
ff: 'Arial',
|
||||
cl: { rgb: '#222222' },
|
||||
...(backgroundColor
|
||||
? {
|
||||
bg: { rgb: backgroundColor },
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
fontStyle: {
|
||||
fontString: '12px Arial',
|
||||
@@ -70,9 +75,9 @@ function createGlyph(content: string, left: number, width = 16) {
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createLine(type: LineType, top: number, withBorder = false) {
|
||||
const glyphA = createGlyph('A', 0);
|
||||
const glyphB = createGlyph('B', 18);
|
||||
function createLine(type: LineType, top: number, withBorder = false, backgroundColor?: string) {
|
||||
const glyphA = createGlyph('A', 0, 16, backgroundColor);
|
||||
const glyphB = createGlyph('B', 18, 16, backgroundColor);
|
||||
const divide = {
|
||||
glyphGroup: [glyphA, glyphB],
|
||||
width: 120,
|
||||
@@ -118,9 +123,9 @@ function createLine(type: LineType, top: number, withBorder = false) {
|
||||
return line;
|
||||
}
|
||||
|
||||
function createPage(pageType: DocumentSkeletonPageType, segmentId: string) {
|
||||
const lineBlock = createLine(LineType.BLOCK, 0);
|
||||
const lineText = createLine(LineType.PARAGRAPH, 24, true);
|
||||
function createPage(pageType: DocumentSkeletonPageType, segmentId: string, backgroundColor?: string) {
|
||||
const lineBlock = createLine(LineType.BLOCK, 0, false, backgroundColor);
|
||||
const lineText = createLine(LineType.PARAGRAPH, 24, true, backgroundColor);
|
||||
const column = {
|
||||
lines: [lineBlock, lineText],
|
||||
left: 0,
|
||||
@@ -281,7 +286,7 @@ describe('documents render', () => {
|
||||
setDocsTableRenderViewportProvider(null);
|
||||
});
|
||||
|
||||
it('uses explicit table cell border width and skips no-border markers', () => {
|
||||
it('uses explicit table cell border width inside table render path', () => {
|
||||
const skeleton = { getSkeletonData: () => ({ pages: [] }) } as any;
|
||||
const documents = new Documents('docs-border', skeleton, {
|
||||
pageLayoutType: PageLayoutType.VERTICAL,
|
||||
@@ -946,7 +951,6 @@ describe('documents render', () => {
|
||||
expect(pageEvents.length).toBe(1);
|
||||
expect(clearCache).toHaveBeenCalled();
|
||||
expect(lineDraw).toHaveBeenCalled();
|
||||
expect(bgDraw).toHaveBeenCalled();
|
||||
expect(spanDraw).toHaveBeenCalled();
|
||||
|
||||
documents.draw(canvas.getContext(), {
|
||||
@@ -964,4 +968,52 @@ describe('documents render', () => {
|
||||
|
||||
documents.dispose();
|
||||
});
|
||||
|
||||
it('merges adjacent glyph backgrounds with the same color into one draw per line', () => {
|
||||
const bodyPage = createPage(DocumentSkeletonPageType.BODY, '', '#d9eaf7');
|
||||
const skeletonData = {
|
||||
pages: [bodyPage],
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
};
|
||||
bodyPage.parent = skeletonData;
|
||||
|
||||
const skeleton = {
|
||||
getSkeletonData: () => skeletonData,
|
||||
} as any;
|
||||
|
||||
const documents = new Documents('docs-merged-background', skeleton, {
|
||||
pageLayoutType: PageLayoutType.VERTICAL,
|
||||
pageMarginLeft: 0,
|
||||
pageMarginTop: 0,
|
||||
});
|
||||
documents.transformByState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 260,
|
||||
height: 180,
|
||||
});
|
||||
scene.addObject(documents, 1);
|
||||
|
||||
const bgDraw = vi.fn();
|
||||
vi.spyOn(documents as any, 'getExtensionsByOrder').mockReturnValue([
|
||||
{
|
||||
uKey: 'DefaultDocsBackgroundExtension',
|
||||
type: DOCS_EXTENSION_TYPE.SPAN,
|
||||
extensionOffset: {},
|
||||
clearCache: vi.fn(),
|
||||
draw: bgDraw,
|
||||
},
|
||||
] as any);
|
||||
|
||||
documents.draw(canvas.getContext(), {
|
||||
viewBound: { left: 0, top: 0, right: 900, bottom: 700 },
|
||||
cacheBound: { left: 0, top: 0, right: 900, bottom: 700 },
|
||||
} as any);
|
||||
|
||||
expect(bgDraw).toHaveBeenCalledTimes(1);
|
||||
expect(bgDraw.mock.calls.map((call) => call[2].width)).toEqual([34]);
|
||||
|
||||
documents.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import { Vector2 } from '../../basics/vector2';
|
||||
import { DocumentsSpanAndLineExtensionRegistry } from '../extension';
|
||||
import { DocComponent } from './doc-component';
|
||||
import { DOCS_EXTENSION_TYPE } from './doc-extension';
|
||||
import { collectBackgroundGlyphRuns } from './extensions/background-runs';
|
||||
import { getTableIdAndSliceIndex } from './layout/block/table';
|
||||
import { Liquid } from './liquid';
|
||||
import { getDocsTableRenderViewport, hasDocsTableHorizontalViewport } from './table-render-viewport';
|
||||
@@ -371,37 +372,16 @@ export class Documents extends DocComponent {
|
||||
this._drawLiquid.translateSave();
|
||||
this._drawLiquid.translateDivide(divide, isVertical && wrapStrategy === WrapStrategy.WRAP, verticalAlign, rotatedHeightStore);
|
||||
|
||||
// Draw text background.
|
||||
for (const glyph of glyphGroup) {
|
||||
if (!glyph.content || glyph.content.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { width: spanWidth, left: spanLeft } = glyph;
|
||||
|
||||
const { x: translateX, y: translateY } = this._drawLiquid;
|
||||
|
||||
const originTranslate = Vector2.create(translateX, translateY);
|
||||
|
||||
const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2);
|
||||
|
||||
const spanStartPoint = calculateRectRotate(
|
||||
originTranslate.addByPoint(spanLeft, 0),
|
||||
centerPoint,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
alignOffset
|
||||
);
|
||||
|
||||
const extensionOffset: IExtensionConfig = {
|
||||
spanStartPoint,
|
||||
};
|
||||
|
||||
if (backgroundExtension) {
|
||||
backgroundExtension.extensionOffset = extensionOffset;
|
||||
backgroundExtension.draw(ctx, parentScale, glyph);
|
||||
}
|
||||
}
|
||||
this._drawGlyphGroupBackgrounds(
|
||||
ctx,
|
||||
parentScale,
|
||||
glyphGroup,
|
||||
lineHeight,
|
||||
alignOffset,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
backgroundExtension
|
||||
);
|
||||
|
||||
// Draw text\border\lines etc.
|
||||
for (const glyph of glyphGroup) {
|
||||
@@ -668,6 +648,42 @@ export class Documents extends DocComponent {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private _drawGlyphGroupBackgrounds(
|
||||
ctx: UniverRenderingContext,
|
||||
parentScale: IScale,
|
||||
glyphGroup: IDocumentSkeletonGlyph[],
|
||||
lineHeight: number,
|
||||
alignOffset: Vector2,
|
||||
centerAngle: number,
|
||||
vertexAngle: number,
|
||||
backgroundExtension: Nullable<ComponentExtension<IDocumentSkeletonGlyph | IDocumentSkeletonLine, DOCS_EXTENSION_TYPE, IBoundRectNoAngle[]>>
|
||||
) {
|
||||
if (!backgroundExtension || this._drawLiquid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundRuns = collectBackgroundGlyphRuns(glyphGroup);
|
||||
|
||||
for (const backgroundRun of backgroundRuns) {
|
||||
const { glyph, left: spanLeft, width: spanWidth } = backgroundRun;
|
||||
const { x: translateX, y: translateY } = this._drawLiquid;
|
||||
const originTranslate = Vector2.create(translateX, translateY);
|
||||
const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2);
|
||||
const spanStartPoint = calculateRectRotate(
|
||||
originTranslate.addByPoint(spanLeft, 0),
|
||||
centerPoint,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
alignOffset
|
||||
);
|
||||
|
||||
backgroundExtension.extensionOffset = {
|
||||
spanStartPoint,
|
||||
};
|
||||
backgroundExtension.draw(ctx, parentScale, glyph);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: @JOCS, DRY!!!
|
||||
private _drawTableCell(
|
||||
ctx: UniverRenderingContext,
|
||||
@@ -747,37 +763,16 @@ export class Documents extends DocComponent {
|
||||
this._drawLiquid.translateSave();
|
||||
this._drawLiquid.translateDivide(divide);
|
||||
|
||||
// Draw text background.
|
||||
for (const glyph of glyphGroup) {
|
||||
if (!glyph.content || glyph.content.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { width: spanWidth, left: spanLeft } = glyph;
|
||||
|
||||
const { x: translateX, y: translateY } = this._drawLiquid;
|
||||
|
||||
const originTranslate = Vector2.create(translateX, translateY);
|
||||
|
||||
const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2);
|
||||
|
||||
const spanStartPoint = calculateRectRotate(
|
||||
originTranslate.addByPoint(spanLeft, 0),
|
||||
centerPoint,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
alignOffset
|
||||
);
|
||||
|
||||
const extensionOffset: IExtensionConfig = {
|
||||
spanStartPoint,
|
||||
};
|
||||
|
||||
if (backgroundExtension) {
|
||||
backgroundExtension.extensionOffset = extensionOffset;
|
||||
backgroundExtension.draw(ctx, parentScale, glyph);
|
||||
}
|
||||
}
|
||||
this._drawGlyphGroupBackgrounds(
|
||||
ctx,
|
||||
parentScale,
|
||||
glyphGroup,
|
||||
lineHeight,
|
||||
alignOffset,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
backgroundExtension
|
||||
);
|
||||
|
||||
// Draw text\border\lines etc.
|
||||
for (const glyph of glyphGroup) {
|
||||
@@ -1042,37 +1037,16 @@ export class Documents extends DocComponent {
|
||||
this._drawLiquid.translateSave();
|
||||
this._drawLiquid.translateDivide(divide);
|
||||
|
||||
// Draw text background.
|
||||
for (const glyph of glyphGroup) {
|
||||
if (!glyph.content || glyph.content.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { width: spanWidth, left: spanLeft } = glyph;
|
||||
|
||||
const { x: translateX, y: translateY } = this._drawLiquid;
|
||||
|
||||
const originTranslate = Vector2.create(translateX, translateY);
|
||||
|
||||
const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2);
|
||||
|
||||
const spanStartPoint = calculateRectRotate(
|
||||
originTranslate.addByPoint(spanLeft, 0),
|
||||
centerPoint,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
alignOffset
|
||||
);
|
||||
|
||||
const extensionOffset: IExtensionConfig = {
|
||||
spanStartPoint,
|
||||
};
|
||||
|
||||
if (backgroundExtension) {
|
||||
backgroundExtension.extensionOffset = extensionOffset;
|
||||
backgroundExtension.draw(ctx, parentScale, glyph);
|
||||
}
|
||||
}
|
||||
this._drawGlyphGroupBackgrounds(
|
||||
ctx,
|
||||
parentScale,
|
||||
glyphGroup,
|
||||
lineHeight,
|
||||
alignOffset,
|
||||
centerAngle,
|
||||
vertexAngle,
|
||||
backgroundExtension
|
||||
);
|
||||
|
||||
// Draw text\border\lines etc.
|
||||
for (const glyph of glyphGroup) {
|
||||
@@ -1153,14 +1127,16 @@ export class Documents extends DocComponent {
|
||||
* In Excel, if horizontal alignment is not specified,
|
||||
* rotated text aligns to the right when rotated downwards and aligns to the left when rotated upwards.
|
||||
*/
|
||||
if (horizontalAlign === HorizontalAlign.UNSPECIFIED) {
|
||||
let resolvedHorizontalAlign = horizontalAlign;
|
||||
|
||||
if (resolvedHorizontalAlign === HorizontalAlign.UNSPECIFIED) {
|
||||
if (centerAngleDeg === VERTICAL_ROTATE_ANGLE && vertexAngleDeg === VERTICAL_ROTATE_ANGLE) {
|
||||
horizontalAlign = HorizontalAlign.CENTER;
|
||||
resolvedHorizontalAlign = HorizontalAlign.CENTER;
|
||||
} else if ((vertexAngleDeg > 0 && vertexAngleDeg !== VERTICAL_ROTATE_ANGLE) || vertexAngleDeg === -VERTICAL_ROTATE_ANGLE) {
|
||||
/**
|
||||
* https://github.com/dream-num/univer-pro/issues/334
|
||||
*/
|
||||
horizontalAlign = HorizontalAlign.RIGHT;
|
||||
resolvedHorizontalAlign = HorizontalAlign.RIGHT;
|
||||
} else {
|
||||
/**
|
||||
* sheet cell type, In a spreadsheet cell, without any alignment settings applied,
|
||||
@@ -1169,19 +1145,19 @@ export class Documents extends DocComponent {
|
||||
* and Boolean values should be center-aligned.
|
||||
*/
|
||||
if (cellValueType === CellValueType.NUMBER) {
|
||||
horizontalAlign = HorizontalAlign.RIGHT;
|
||||
resolvedHorizontalAlign = HorizontalAlign.RIGHT;
|
||||
} else if (cellValueType === CellValueType.BOOLEAN) {
|
||||
horizontalAlign = HorizontalAlign.CENTER;
|
||||
resolvedHorizontalAlign = HorizontalAlign.CENTER;
|
||||
} else {
|
||||
horizontalAlign = HorizontalAlign.LEFT;
|
||||
resolvedHorizontalAlign = HorizontalAlign.LEFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let offsetLeft = 0;
|
||||
if (horizontalAlign === HorizontalAlign.CENTER) {
|
||||
if (resolvedHorizontalAlign === HorizontalAlign.CENTER) {
|
||||
offsetLeft = (this.width - pageWidth) / 2;
|
||||
} else if (horizontalAlign === HorizontalAlign.RIGHT) {
|
||||
} else if (resolvedHorizontalAlign === HorizontalAlign.RIGHT) {
|
||||
offsetLeft = this.width - pageWidth - pagePaddingRight;
|
||||
} else {
|
||||
offsetLeft = pagePaddingLeft;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 { IDocumentSkeletonGlyph } from '../../../basics/i-document-skeleton-cached';
|
||||
import { getColorStyle } from '@univerjs/core';
|
||||
|
||||
export interface IBackgroundGlyphRun {
|
||||
glyph: IDocumentSkeletonGlyph;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export function collectBackgroundGlyphRuns(glyphGroup: IDocumentSkeletonGlyph[]): IBackgroundGlyphRun[] {
|
||||
const runs: IBackgroundGlyphRun[] = [];
|
||||
|
||||
let activeGlyph: IDocumentSkeletonGlyph | null = null;
|
||||
let activeColor = '';
|
||||
let activeLeft = 0;
|
||||
let activeRight = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (!activeGlyph) {
|
||||
return;
|
||||
}
|
||||
|
||||
runs.push({
|
||||
glyph: {
|
||||
...activeGlyph,
|
||||
width: activeRight - activeLeft,
|
||||
},
|
||||
left: activeLeft,
|
||||
width: activeRight - activeLeft,
|
||||
});
|
||||
|
||||
activeGlyph = null;
|
||||
activeColor = '';
|
||||
activeLeft = 0;
|
||||
activeRight = 0;
|
||||
};
|
||||
|
||||
for (const glyph of glyphGroup) {
|
||||
if (!glyph.content || glyph.content === '\r') {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
const backgroundColor = glyph.ts?.bg ? getColorStyle(glyph.ts.bg) : '';
|
||||
if (!backgroundColor) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
const glyphLeft = glyph.left;
|
||||
const glyphRight = glyph.left + glyph.width;
|
||||
|
||||
if (activeGlyph && activeColor === backgroundColor) {
|
||||
activeRight = Math.max(activeRight, glyphRight);
|
||||
continue;
|
||||
}
|
||||
|
||||
flush();
|
||||
activeGlyph = glyph;
|
||||
activeColor = backgroundColor;
|
||||
activeLeft = glyphLeft;
|
||||
activeRight = glyphRight;
|
||||
}
|
||||
|
||||
flush();
|
||||
|
||||
return runs;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export function DesignTinyMenuGroup({ items, columns, sizeVariant = 'default', l
|
||||
dark:!univer-text-gray-200
|
||||
`,
|
||||
isCompactParagraphVariant
|
||||
? 'univer-size-4'
|
||||
? 'univer-size-5'
|
||||
: isParagraphTVariant
|
||||
? 'univer-size-5'
|
||||
: 'univer-size-4',
|
||||
|
||||
@@ -44,6 +44,6 @@ describe('DesignTinyMenuGroup', () => {
|
||||
expect(group?.className ?? '').toContain('univer-p-0');
|
||||
expect(button?.className ?? '').toContain('univer-size-6');
|
||||
expect(button?.className ?? '').toContain('univer-rounded-sm');
|
||||
expect(icon?.className ?? '').toContain('univer-size-4');
|
||||
expect(icon?.className ?? '').toContain('univer-size-5');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -870,6 +870,12 @@ function ContextMenuMenuItem(props: IContextMenuMenuItemProps) {
|
||||
onClick={() => {
|
||||
clearSubmenuCloseTimer();
|
||||
if (hasSubmenu) {
|
||||
if (headerAction) {
|
||||
setSubmenuPositionReady(false);
|
||||
setActiveSubmenuKey(menuKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canExecuteItem) {
|
||||
const item = menuItem as IDisplayMenuItem<IMenuButtonItem>;
|
||||
onOptionSelect?.({
|
||||
|
||||
@@ -181,7 +181,6 @@ describe('ContextMenuPanel', () => {
|
||||
sizeVariant: 'paragraph-t',
|
||||
}));
|
||||
|
||||
const tinyGroups = screen.getAllByTestId('tiny-menu-group');
|
||||
const sharedCluster = container.querySelector('.univer-gap-0') as HTMLDivElement | null;
|
||||
|
||||
expect(sharedCluster).not.toBeNull();
|
||||
@@ -318,6 +317,60 @@ describe('ContextMenuPanel', () => {
|
||||
expect(document.querySelector('button[title="header-action"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('opens a header action selector submenu instead of immediately executing its current value', () => {
|
||||
dependencyMap.clear();
|
||||
tinyMenuGroupSpy.mockClear();
|
||||
const onOptionSelect = vi.fn();
|
||||
|
||||
dependencyMap.set(IMenuManagerService, {
|
||||
menuChanged$: new BehaviorSubject<void>(undefined),
|
||||
getMenuByPositionKey: vi.fn(() => [{
|
||||
key: 'colors',
|
||||
order: 0,
|
||||
title: 'docs-ui.toolbar.textColor.main',
|
||||
headerActionItem: {
|
||||
id: 'header-action',
|
||||
type: MenuItemType.BUTTON_SELECTOR,
|
||||
icon: 'HeaderTextColorIcon',
|
||||
tooltip: 'header-action',
|
||||
selections: [{
|
||||
label: 'custom-option',
|
||||
value: '#ff0000',
|
||||
}],
|
||||
},
|
||||
children: [{
|
||||
key: 'quickItem',
|
||||
order: 0,
|
||||
item: {
|
||||
id: 'quick-item',
|
||||
type: MenuItemType.BUTTON,
|
||||
},
|
||||
}],
|
||||
}]),
|
||||
});
|
||||
dependencyMap.set(ILayoutService, {
|
||||
rootContainerElement: document.body,
|
||||
});
|
||||
dependencyMap.set(LocaleService, {
|
||||
t: (key: string) => key,
|
||||
direction$: new BehaviorSubject<'ltr'>('ltr'),
|
||||
});
|
||||
|
||||
render(React.createElement(ContextMenuPanel as never, {
|
||||
menuType: 'quick-layout-menu',
|
||||
sizeVariant: 'paragraph-t',
|
||||
onOptionSelect,
|
||||
}));
|
||||
|
||||
const headerActionButton = document.querySelector('button[title="header-action"]') as HTMLButtonElement | null;
|
||||
|
||||
expect(headerActionButton).not.toBeNull();
|
||||
|
||||
fireEvent.click(headerActionButton!);
|
||||
|
||||
expect(onOptionSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders quick layout group titles before icon rows', () => {
|
||||
dependencyMap.clear();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user