fix(docs-ui): clear solid text fill when setting text color (#7558)

This commit is contained in:
Univer
2026-08-19 20:38:41 +08:00
committed by GitHub
parent ea42757676
commit 69a4795bc5
2 changed files with 176 additions and 19 deletions
@@ -14,10 +14,11 @@
* limitations under the License.
*/
import type { DocumentDataModel, ICommand, Injector, ITextStyle, Univer } from '@univerjs/core';
import type { DocumentDataModel, ICommand, IDocumentData, Injector, ITextStyle, Univer } from '@univerjs/core';
import {
BaselineOffset,
BooleanNumber,
createInternalEditorID,
DOC_RANGE_TYPE,
ICommandService,
IUniverInstanceService,
@@ -425,5 +426,118 @@ describe('Test inline format commands', () => {
rgb: null,
});
});
it('clears only solid text fills in a standalone document', async () => {
const docsModel = get(IUniverInstanceService)
.getUnit<DocumentDataModel>('test-doc', UniverInstanceType.UNIVER_DOC)!;
const body = docsModel.getBody()!;
const gradientFill = {
type: 'gradient' as const,
gradient: {
type: 'linear' as const,
stops: [
{ offset: 0, color: '#111111' },
{ offset: 1, color: '#eeeeee' },
],
},
};
const pictureFill = {
type: 'picture' as const,
picture: {
source: 'https://example.com/fill.png',
},
};
body.textRuns = [
{
st: 0,
ed: 2,
ts: {
bl: BooleanNumber.TRUE,
cl: { rgb: '#111111' },
textFill: { type: 'solid', color: '#222222' },
},
},
{
st: 2,
ed: 4,
ts: {
it: BooleanNumber.TRUE,
cl: { rgb: '#111111' },
textFill: gradientFill,
},
},
{
st: 4,
ed: 22,
ts: {
fs: 24,
cl: { rgb: '#111111' },
textFill: pictureFill,
},
},
{
st: 23,
ed: 68,
ts: {
fs: 24,
cl: { rgb: '#111111' },
},
},
];
await commandService.executeCommand(SetInlineFormatTextColorCommand.id, { value: '#ff0000' });
expect(getFormatValueAt('cl', 1)).toStrictEqual({ rgb: '#ff0000' });
expect(getFormatValueAt('bl', 1)).toBe(BooleanNumber.TRUE);
expect(getFormatValueAt('textFill', 1)).toBeUndefined();
expect(getFormatValueAt('textFill', 3)).toStrictEqual(gradientFill);
expect(getFormatValueAt('textFill', 4.5)).toStrictEqual(pictureFill);
await commandService.executeCommand(UndoCommand.id);
expect(getFormatValueAt('cl', 1)).toStrictEqual({ rgb: '#111111' });
expect(getFormatValueAt('textFill', 1)).toStrictEqual({ type: 'solid', color: '#222222' });
await commandService.executeCommand(RedoCommand.id);
expect(getFormatValueAt('cl', 1)).toStrictEqual({ rgb: '#ff0000' });
expect(getFormatValueAt('textFill', 1)).toBeUndefined();
});
it('preserves solid text fills in an internal editor', async () => {
const unitId = createInternalEditorID('shape-text');
const internalDoc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, {
id: unitId,
body: {
dataStream: 'abc\r\n',
textRuns: [{
st: 0,
ed: 3,
ts: {
cl: { rgb: '#111111' },
textFill: { type: 'solid', color: '#222222' },
},
}],
paragraphs: [{ startIndex: 3, paragraphId: 'shape-text-paragraph' }],
},
});
const univerInstanceService = get(IUniverInstanceService);
univerInstanceService.focusUnit(unitId);
const selectionManager = get(DocSelectionManagerService);
selectionManager.__TEST_ONLY_setCurrentSelection({ unitId, subUnitId: unitId });
selectionManager.__TEST_ONLY_add([{
startOffset: 0,
endOffset: 3,
collapsed: false,
isActive: true,
}]);
await commandService.executeCommand(SetInlineFormatTextColorCommand.id, { value: '#ff0000' });
expect(internalDoc.getBody()?.textRuns?.[0].ts?.cl).toStrictEqual({ rgb: '#ff0000' });
expect(internalDoc.getBody()?.textRuns?.[0].ts?.textFill).toStrictEqual({
type: 'solid',
color: '#222222',
});
});
});
});
@@ -35,6 +35,7 @@ import {
getBodySlice,
getRichTextEditPath,
ICommandService,
isInternalEditorID,
IUniverInstanceService,
JSONX,
MemoryCursor,
@@ -42,9 +43,11 @@ import {
TextXActionType,
Tools,
UniverInstanceType,
UpdateDocsAttributeType,
} from '@univerjs/core';
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { DocMenuStyleService } from '../../services/doc-menu-style.service';
import { IEditorService } from '../../services/editor/editor-manager.service';
function handleInlineFormat(
preCommandId: string,
@@ -280,6 +283,7 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
handler: async (accessor, params: ISetInlineFormatCommandParams) => {
const { value, preCommandId } = params;
const commandService = accessor.get(ICommandService);
const editorService = accessor.has(IEditorService) ? accessor.get(IEditorService) : null;
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const docMenuStyleService = accessor.get(DocMenuStyleService);
@@ -377,6 +381,32 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
const textX = new TextX();
const jsonX = JSONX.getInstance();
const textStylePatch = formatPatch ?? {
[COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]]: formatValue,
};
const shouldClearSolidTextFill = preCommandId === SetInlineFormatTextColorCommand.id
&& !isInternalEditorID(unitId)
&& !editorService?.isEditor(unitId);
const pushTextStyleUpdate = (len: number, ts: Partial<ITextStyle>, replace = false) => {
if (len <= 0) {
return;
}
textX.push({
t: TextXActionType.RETAIN,
body: {
dataStream: '',
textRuns: [{
st: 0,
ed: len,
ts,
}],
},
len,
...(replace ? { coverType: UpdateDocsAttributeType.REPLACE } : {}),
});
};
const memoryCursor = new MemoryCursor();
memoryCursor.reset();
@@ -416,19 +446,6 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
continue;
}
const body: IDocumentBody = {
dataStream: '',
textRuns: [
{
st: 0,
ed: endOffset - startOffset,
ts: formatPatch ?? {
[COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]]: formatValue,
},
},
],
};
const len = startOffset - memoryCursor.cursor;
if (len !== 0) {
@@ -438,11 +455,37 @@ export const SetInlineFormatCommand: ICommand<ISetInlineFormatCommandParams> = {
});
}
textX.push({
t: TextXActionType.RETAIN,
body,
len: endOffset - startOffset,
});
if (shouldClearSolidTextFill) {
let currentOffset = startOffset;
for (const textRun of body.textRuns ?? []) {
if (textRun.ts?.textFill?.type !== 'solid') {
continue;
}
const solidStart = Math.max(currentOffset, textRun.st);
const solidEnd = Math.min(endOffset, textRun.ed);
if (solidStart >= solidEnd) {
continue;
}
pushTextStyleUpdate(solidStart - currentOffset, textStylePatch);
const replacementStyle = {
...textRun.ts,
...textStylePatch,
};
delete replacementStyle.textFill;
pushTextStyleUpdate(solidEnd - solidStart, replacementStyle, true);
currentOffset = solidEnd;
}
pushTextStyleUpdate(endOffset - currentOffset, textStylePatch);
} else {
pushTextStyleUpdate(endOffset - startOffset, textStylePatch);
}
memoryCursor.reset();
memoryCursor.moveCursor(endOffset);