Skip to content

Commit e2b94c1

Browse files
author
r.sergeev
committed
feat: underline selection
1 parent 6b7bef5 commit e2b94c1

File tree

7 files changed

+217
-13
lines changed

7 files changed

+217
-13
lines changed

packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
*/
1616

1717
import type { DocumentDataModel, ICommand, Injector, IStyleBase, Univer } from '@univerjs/core';
18-
import { BooleanNumber, ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
18+
import {
19+
BooleanNumber,
20+
ICommandService,
21+
IUniverInstanceService,
22+
RedoCommand,
23+
TextDecoration,
24+
UndoCommand,
25+
UniverInstanceType,
26+
} from '@univerjs/core';
1927
import { DocSelectionManagerService, RichTextEditingMutation, SetTextSelectionsOperation } from '@univerjs/docs';
2028
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2129
import {
@@ -146,17 +154,18 @@ describe('Test inline format commands', () => {
146154
const commandParams = {
147155
segmentId: '',
148156
preCommandId: SetInlineFormatUnderlineCommand.id,
157+
value: { s: BooleanNumber.TRUE, t: TextDecoration.SINGLE },
149158
};
150159

151160
await commandService.executeCommand(SetInlineFormatCommand.id, commandParams);
152161

153-
expect(getFormatValueAt('ul', 1)).toStrictEqual({ s: BooleanNumber.TRUE });
162+
expect(getFormatValueAt('ul', 1)).toStrictEqual({ s: BooleanNumber.TRUE, t: TextDecoration.SINGLE });
154163

155164
await commandService.executeCommand(UndoCommand.id);
156165
expect(getFormatValueAt('ul', 1)).toStrictEqual(undefined);
157166

158167
await commandService.executeCommand(RedoCommand.id);
159-
expect(getFormatValueAt('ul', 1)).toStrictEqual({ s: BooleanNumber.TRUE });
168+
expect(getFormatValueAt('ul', 1)).toStrictEqual({ s: BooleanNumber.TRUE, t: TextDecoration.SINGLE });
160169
});
161170
});
162171

@@ -167,17 +176,18 @@ describe('Test inline format commands', () => {
167176
const commandParams = {
168177
segmentId: '',
169178
preCommandId: SetInlineFormatStrikethroughCommand.id,
179+
value: { s: BooleanNumber.TRUE, t: TextDecoration.SINGLE },
170180
};
171181

172182
await commandService.executeCommand(SetInlineFormatCommand.id, commandParams);
173183

174-
expect(getFormatValueAt('st', 1)).toStrictEqual({ s: BooleanNumber.TRUE });
184+
expect(getFormatValueAt('st', 1)).toStrictEqual({ s: BooleanNumber.TRUE, t: TextDecoration.SINGLE });
175185

176186
await commandService.executeCommand(UndoCommand.id);
177187
expect(getFormatValueAt('st', 1)).toStrictEqual(undefined);
178188

179189
await commandService.executeCommand(RedoCommand.id);
180-
expect(getFormatValueAt('st', 1)).toStrictEqual({ s: BooleanNumber.TRUE });
190+
expect(getFormatValueAt('st', 1)).toStrictEqual({ s: BooleanNumber.TRUE, t: TextDecoration.SINGLE });
181191
});
182192
});
183193

packages/docs-ui/src/commands/commands/inline-format.command.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
280280
switch (preCommandId) {
281281
case SetInlineFormatBoldCommand.id: // fallthrough
282282
case SetInlineFormatItalicCommand.id: // fallthrough
283-
case SetInlineFormatUnderlineCommand.id: // fallthrough
284283
case SetInlineFormatStrikethroughCommand.id: // fallthrough
285284
case SetInlineFormatSubscriptCommand.id: // fallthrough
286285
case SetInlineFormatSuperscriptCommand.id: {
@@ -319,6 +318,10 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
319318
};
320319
break;
321320
}
321+
case SetInlineFormatUnderlineCommand.id: {
322+
formatValue = value;
323+
break;
324+
}
322325

323326
default: {
324327
throw new Error(`Unknown command: ${preCommandId} in handleInlineFormat`);
@@ -431,6 +434,7 @@ function getReverseFormatValue(ts: Nullable<ITextStyle>, key: keyof IStyleBase,
431434
}
432435
: {
433436
s: BooleanNumber.TRUE,
437+
t: ts?.ul?.t,
434438
};
435439
}
436440

packages/docs-ui/src/components/list-type-picker/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ import { COMPONENT_PREFIX } from '../const';
1919
export { BulletListTypePicker, OrderListTypePicker } from './picker';
2020
export const ORDER_LIST_TYPE_COMPONENT = `${COMPONENT_PREFIX}_ORDER_LIST_TYPE_COMPONENT`;
2121
export const BULLET_LIST_TYPE_COMPONENT = `${COMPONENT_PREFIX}_BULLET_LIST_TYPE_COMPONENT`;
22+
export const UNDERLINE_TYPE_COMPONENT = `${COMPONENT_PREFIX}_UNDERLINE_TYPE_COMPONENT`;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright 2023-present DreamNum Co., Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { COMPONENT_PREFIX } from '../const';
18+
19+
export { UnderlineTypePicker } from './picker';
20+
export const UNDERLINE_TYPE_COMPONENT = `${COMPONENT_PREFIX}_UNDERLINE_TYPE_COMPONENT`;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright 2023-present DreamNum Co., Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ITextDecoration } from '@univerjs/core';
18+
import { BooleanNumber, TextDecoration } from '@univerjs/core';
19+
import { clsx } from '@univerjs/design';
20+
import { CheckMarkIcon } from '@univerjs/icons';
21+
22+
export interface ITextDecorationTypePickerBaseProps {
23+
value?: ITextDecoration;
24+
onChange: (value: ITextDecoration | undefined) => void;
25+
}
26+
27+
interface ITextDecorationTypePickerProps extends ITextDecorationTypePickerBaseProps {
28+
options: { value: TextDecoration; img: string }[];
29+
}
30+
31+
export const TextDecorationTypePicker = (props: ITextDecorationTypePickerProps) => {
32+
const { value, onChange, options } = props;
33+
34+
return (
35+
<div className="univer-grid univer-gap-2">
36+
{options.map((item) => {
37+
return (
38+
<div className="univer-grid" key={item.value}>
39+
{ value?.t === item.value && (
40+
<CheckMarkIcon
41+
className="univer-absolute univer-mt-0.5 univer-text-primary-600"
42+
/>
43+
) }
44+
<a
45+
className={clsx(`
46+
univer-ml-5 univer-block univer-h-5 univer-w-[72px] univer-cursor-pointer
47+
univer-overflow-hidden univer-rounded univer-transition-all
48+
hover:univer-bg-gray-100
49+
`)}
50+
onClick={() => {
51+
onChange({ s: BooleanNumber.TRUE, t: item.value });
52+
}}
53+
>
54+
<img
55+
className="univer-size-full"
56+
src={item.img}
57+
draggable={false}
58+
/>
59+
</a>
60+
</div>
61+
);
62+
})}
63+
</div>
64+
);
65+
};
66+
67+
const underlineOptions = [ // TODO: add DASH_DOT_DOT_HEAVY, DASH_DOT_HEAVY, DASHED_HEAVY, DASH_LONG, DASH_LONG_HEAVY, DOT_DASH, DOT_DOT_DASH, WAVY_HEAVY
68+
{
69+
value: TextDecoration.SINGLE,
70+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bGluZSB4MT0iMCIgeTE9IjYiIHgyPSI3MiIgeTI9IjYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiAvPgo8L3N2Zz4=',
71+
},
72+
{
73+
value: TextDecoration.DOUBLE,
74+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bGluZSB4MT0iMCIgeTE9IjQiIHgyPSI3MiIgeTI9IjQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxLjUiIC8+CiAgPGxpbmUgeDE9IjAiIHkxPSI4IiB4Mj0iNzIiIHkyPSI4IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiAvPgo8L3N2Zz4=',
75+
},
76+
{
77+
value: TextDecoration.DOTTED,
78+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bGluZSB4MT0iMCIgeTE9IjYiIHgyPSI3MiIgeTI9IjYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1kYXNoYXJyYXk9IjEsNSIgLz4KPC9zdmc+',
79+
},
80+
{
81+
value: TextDecoration.DASH,
82+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bGluZSB4MT0iMCIgeTE9IjYiIHgyPSI3MiIgeTI9IjYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtZGFzaGFycmF5PSI2LDQiIC8+Cjwvc3ZnPg==',
83+
},
84+
{
85+
value: TextDecoration.THICK,
86+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMCIgeTE9IjYiIHgyPSI3MiIgeTI9IjYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+',
87+
},
88+
{
89+
value: TextDecoration.WAVE,
90+
img: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCA3MiAxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMCw2IFE2LDIgMTIsNiBUMjQsNiBUMzYsNiBU NDgsNiBU NjAsNiBU NzIsNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+',
91+
},
92+
{
93+
value: TextDecoration.WAVY_DOUBLE,
94+
img: 'data:image/svg+xml,%3Csvg width=\'72\' height=\'12\' viewBox=\'0 0 72 12\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M0,3 C2,1 4,1 6,3 C8,5 10,5 12,3 C14,1 16,1 18,3 C20,5 22,5 24,3 C26,1 28,1 30,3 C32,5 34,5 36,3 C38,1 40,1 42,3 C44,5 46,5 48,3 C50,1 52,1 54,3 C56,5 58,5 60,3 C62,1 64,1 66,3 C68,5 70,5 72,3\' fill=\'none\' stroke=\'%23000\' stroke-width=\'1\'/%3E%3Cpath d=\'M0,9 C2,7 4,7 6,9 C8,11 10,11 12,9 C14,7 16,7 18,9 C20,11 22,11 24,9 C26,7 28,7 30,9 C32,11 34,11 36,9 C38,7 40,7 42,9 C44,11 46,11 48,9 C50,7 52,7 54,9 C56,11 58,11 60,9 C62,7 64,7 66,9 C68,11 70,11 72,9\' fill=\'none\' stroke=\'%23000\' stroke-width=\'1\'/%3E%3C/svg%3E',
95+
},
96+
];
97+
98+
export const UnderlineTypePicker = (props: ITextDecorationTypePickerBaseProps) => {
99+
return (
100+
<TextDecorationTypePicker
101+
{...props}
102+
options={underlineOptions}
103+
/>
104+
);
105+
};

packages/docs-ui/src/controllers/doc-ui.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import { CutIcon, DeleteIcon, DocSettingIcon, TodoListDoubleIcon } from '@univer
2828
import { BuiltInUIPart, ComponentManager, connectInjector, ILayoutService, IMenuManagerService, IShortcutService, IUIPartsService } from '@univerjs/ui';
2929
import { CoreHeaderFooterCommand, OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command';
3030
import { SidebarDocHeaderFooterPanelOperation } from '../commands/operations/doc-header-footer-panel.operation';
31-
import { BULLET_LIST_TYPE_COMPONENT, BulletListTypePicker, ORDER_LIST_TYPE_COMPONENT, OrderListTypePicker } from '../components/list-type-picker';
31+
import { BULLET_LIST_TYPE_COMPONENT, BulletListTypePicker, ORDER_LIST_TYPE_COMPONENT, OrderListTypePicker, UNDERLINE_TYPE_COMPONENT } from '../components/list-type-picker';
3232
import { ParagraphMenu } from '../components/paragraph-menu';
33+
import { UnderlineTypePicker } from '../components/underiline-picker';
3334
import { DocSelectionRenderService } from '../services/selection/doc-selection-render.service';
3435
import { TabShortCut } from '../shortcuts/format.shortcut';
3536
import {
@@ -72,6 +73,7 @@ export class DocUIController extends Disposable {
7273
([
7374
[BULLET_LIST_TYPE_COMPONENT, BulletListTypePicker],
7475
[ORDER_LIST_TYPE_COMPONENT, OrderListTypePicker],
76+
[UNDERLINE_TYPE_COMPONENT, UnderlineTypePicker],
7577
['TodoListDoubleIcon', TodoListDoubleIcon],
7678
['doc.paragraph.menu', ParagraphMenu],
7779
['CutIcon', CutIcon],

packages/docs-ui/src/controllers/menu/menu.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { DocumentDataModel, IAccessor, PresetListType } from '@univerjs/core';
17+
import type { DocumentDataModel, IAccessor, ITextDecoration, PresetListType } from '@univerjs/core';
1818
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
1919
import type { IMenuButtonItem, IMenuItem, IMenuSelectorItem } from '@univerjs/ui';
2020
import type { Subscription } from 'rxjs';
@@ -30,6 +30,7 @@ import {
3030
IUniverInstanceService,
3131
NAMED_STYLE_MAP,
3232
NamedStyleType,
33+
TextDecoration,
3334
ThemeService,
3435
Tools,
3536
UniverInstanceType,
@@ -66,7 +67,7 @@ import { SwitchDocModeCommand } from '../../commands/commands/switch-doc-mode.co
6667
import { DocCreateTableOperation } from '../../commands/operations/doc-create-table.operation';
6768
import { DocOpenPageSettingCommand } from '../../commands/operations/open-page-setting.operation';
6869
import { getCommandSkeleton } from '../../commands/util';
69-
import { BULLET_LIST_TYPE_COMPONENT, ORDER_LIST_TYPE_COMPONENT } from '../../components/list-type-picker';
70+
import { BULLET_LIST_TYPE_COMPONENT, ORDER_LIST_TYPE_COMPONENT, UNDERLINE_TYPE_COMPONENT } from '../../components/list-type-picker';
7071
import { DocMenuStyleService } from '../../services/doc-menu-style.service';
7172

7273
function getInsertTableHiddenObservable(
@@ -318,15 +319,50 @@ export function ItalicMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
318319
};
319320
}
320321

321-
export function UnderlineMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
322+
export function UnderlineMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<ITextDecoration, ITextDecoration | undefined> {
322323
const commandService = accessor.get(ICommandService);
323324

324325
return {
325326
id: SetInlineFormatUnderlineCommand.id,
326-
type: MenuItemType.BUTTON,
327+
type: MenuItemType.BUTTON_SELECTOR,
327328
icon: 'UnderlineIcon',
328-
title: 'Set underline',
329-
tooltip: 'toolbar.underline',
329+
tooltip: 'toolbar.underline.none',
330+
selections: [
331+
{
332+
label: {
333+
name: UNDERLINE_TYPE_COMPONENT,
334+
hoverable: false,
335+
selectable: false,
336+
},
337+
value$: new Observable<ITextDecoration>((subscriber) => {
338+
const defaultValue = DEFAULT_STYLES.ul;
339+
340+
const calc = () => {
341+
const textRun = getFontStyleAtCursor(accessor);
342+
343+
if (!textRun) {
344+
subscriber.next(defaultValue);
345+
return;
346+
}
347+
const ul = textRun.ts?.ul;
348+
subscriber.next(ul ?? defaultValue);
349+
};
350+
351+
const disposable = commandService.onCommandExecuted((c) => {
352+
const id = c.id;
353+
354+
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) {
355+
const underline = (c.params as { value: ITextDecoration }).value;
356+
subscriber.next(underline ?? defaultValue);
357+
}
358+
});
359+
360+
calc();
361+
return disposable.dispose;
362+
}
363+
),
364+
},
365+
],
330366
activated$: new Observable<boolean>((subscriber) => {
331367
const calc = () => {
332368
const textRun = getFontStyleAtCursor(accessor);
@@ -352,6 +388,32 @@ export function UnderlineMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
352388

353389
return disposable.dispose;
354390
}),
391+
value$: new Observable<ITextDecoration>((subscriber) => {
392+
const defaultValue = DEFAULT_STYLES.ul;
393+
394+
const calc = () => {
395+
const textRun = getFontStyleAtCursor(accessor);
396+
397+
if (!textRun) {
398+
subscriber.next(defaultValue);
399+
return;
400+
}
401+
402+
const ul = textRun?.ts?.ul;
403+
subscriber.next(ul?.s === BooleanNumber.FALSE ? { s: BooleanNumber.TRUE, t: TextDecoration.SINGLE } : defaultValue);
404+
};
405+
const disposable = commandService.onCommandExecuted((c) => {
406+
const id = c.id;
407+
408+
if (id === SetInlineFormatCommand.id) {
409+
const ul = (c.params as { value: ITextDecoration }).value;
410+
subscriber.next(ul?.s === BooleanNumber.FALSE ? { s: BooleanNumber.TRUE, t: TextDecoration.SINGLE } : defaultValue);
411+
}
412+
});
413+
414+
calc();
415+
return disposable.dispose;
416+
}),
355417
disabled$: disableMenuWhenNoDocRange(accessor),
356418
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
357419
};

0 commit comments

Comments
 (0)