feat(ui): add font management service & support appending custom font family (#6237)

This commit is contained in:
白熱
2025-12-02 20:18:47 +08:00
committed by GitHub
parent 2feee7e401
commit 7c1c2c9b20
25 changed files with 556 additions and 272 deletions
+8
View File
@@ -125,6 +125,10 @@ function createNewInstance() {
[UniverUIPlugin, {
container: 'app',
// ribbonType: 'classic',
customFontFamily: [
{ value: 'PingFang SC', label: '苹方(简)', category: 'sans-serif' },
{ value: 'Helvetica Neue', label: 'Helvetica Neue', category: 'sans-serif' },
],
}],
[UniverWebComponentAdapterPlugin],
[UniverVue3AdapterPlugin],
@@ -192,6 +196,10 @@ function createNewInstance() {
window.univer = univer;
window.univerAPI = FUniver.newAPI(univer);
// window.univerAPI.addFonts([
// { value: 'PingFang SC', label: '苹方(简)', category: 'sans-serif' },
// { value: 'Helvetica Neue', label: 'Helvetica Neue', category: 'sans-serif' },
// ]);
customRegisterEvent(univer, window.univerAPI!);
customRangePopups(univer, window.univerAPI!);
@@ -46,7 +46,6 @@ import {
COMMON_LABEL_COMPONENT,
FONT_FAMILY_COMPONENT,
FONT_FAMILY_ITEM_COMPONENT,
FONT_FAMILY_LIST,
FONT_SIZE_COMPONENT,
FONT_SIZE_LIST,
getMenuHiddenObservable,
@@ -480,12 +479,16 @@ export function FontFamilySelectorMenuItemFactory(accessor: IAccessor): IMenuSel
tooltip: 'toolbar.font',
type: MenuItemType.SELECTOR,
label: FONT_FAMILY_COMPONENT,
selections: FONT_FAMILY_LIST.map((item) => ({
selections: [{
label: {
name: FONT_FAMILY_ITEM_COMPONENT,
hoverable: false,
selectable: false,
props: {
id: SetInlineFormatFontFamilyCommand.id,
},
},
value: item.value,
})),
}],
// disabled$: getCurrentSheetDisabled$(accessor),
value$: new Observable((subscriber) => {
const defaultValue = DEFAULT_STYLES.ff;
@@ -210,15 +210,13 @@ export const FactoryOtherMenuItem = (accessor: IAccessor): IMenuSelectorItem =>
tooltip: 'sheet.numfmt.title',
type: MenuItemType.SELECTOR,
slot: true,
selections: [
{
label: {
name: OPTIONS_KEY,
hoverable: false,
selectable: false,
},
selections: [{
label: {
name: OPTIONS_KEY,
hoverable: false,
selectable: false,
},
],
}],
value$,
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSetCellStylePermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }),
@@ -27,7 +27,7 @@ import { MENU_OPTIONS } from '../../controllers/menu';
export const MORE_NUMFMT_TYPE_KEY = 'sheet.numfmt.moreNumfmtType';
export const OPTIONS_KEY = 'sheet.numfmt.moreNumfmtType.options';
export const MoreNumfmtType = (props: { value?: string }) => {
export function MoreNumfmtType(props: { value?: string }) {
const { value } = props;
const localeService = useDependency(LocaleService);
const text = value ?? localeService.t('sheet.numfmt.general');
@@ -35,7 +35,7 @@ export const MoreNumfmtType = (props: { value?: string }) => {
return <span className="univer-text-sm">{text}</span>;
};
export const Options = () => {
export function Options() {
const commandService = useDependency(ICommandService);
const localeService = useDependency(LocaleService);
const layoutService = useDependency(ILayoutService);
@@ -66,7 +66,8 @@ export const Options = () => {
const currencySymbol = localeCurrencySymbolMap.get(localeService.getCurrentLocale()) as string;
return MENU_OPTIONS(currencySymbol);
}, [localeService]);
const handleOnclick = (index: number) => {
const handleClick = (index: number) => {
if (index === 0) {
setNumfmt(null);
} else if (index === menuOptions.length - 1) {
@@ -87,6 +88,7 @@ export const Options = () => {
if (item === '|') {
return <Separator key={index} />;
}
return (
<div
key={index}
@@ -96,9 +98,7 @@ export const Options = () => {
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`}
onClick={() => {
handleOnclick(index);
}}
onClick={() => handleClick(index)}
>
<span>{localeService.t(item.label)}</span>
@@ -75,7 +75,6 @@ import {
CutCommand,
FONT_FAMILY_COMPONENT,
FONT_FAMILY_ITEM_COMPONENT,
FONT_FAMILY_LIST,
getMenuHiddenObservable,
IClipboardInterfaceService,
MenuItemType,
@@ -400,13 +399,16 @@ export function FontFamilySelectorMenuItemFactory(accessor: IAccessor): IMenuSel
tooltip: 'toolbar.font',
type: MenuItemType.SELECTOR,
label: FONT_FAMILY_COMPONENT,
selections: FONT_FAMILY_LIST.map((item) => ({
selections: [{
label: {
name: FONT_FAMILY_ITEM_COMPONENT,
hoverable: false,
selectable: false,
props: {
id: SetRangeFontFamilyCommand.id,
},
},
value: item.value,
})),
}],
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookEditablePermission],
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission],
@@ -14,18 +14,72 @@
* limitations under the License.
*/
import type { IFontFamilyItemProps } from './interface';
import { LocaleService } from '@univerjs/core';
import type { IFontConfig } from '../../services/font.service';
import { ICommandService, LocaleService } from '@univerjs/core';
import { Tooltip } from '@univerjs/design';
import { InfoIcon } from '@univerjs/icons';
import { useEffect, useState } from 'react';
import { IFontService } from '../../services/font.service';
import { useDependency } from '../../utils/di';
export const FontFamilyItem = (props: IFontFamilyItemProps) => {
const { value } = props;
export const FontFamilyItem = ({ id, value }: { id: string; value: string }) => {
const commandService = useDependency(ICommandService);
const fontService = useDependency(IFontService);
const [fonts, setFonts] = useState<IFontConfig[]>([]);
useEffect(() => {
const subscription = fontService.fonts$.subscribe((fonts) => {
setFonts(fonts);
});
return () => {
subscription.unsubscribe();
};
}, []);
const localeService = useDependency(LocaleService);
function handleSelectFont(value: string) {
commandService.executeCommand(id, { value });
}
return (
<span className="univer-text-sm" style={{ fontFamily: value }}>
{localeService.t(`fontFamily.${(`${value ?? ''}`).replace(/\s/g, '')}`)}
</span>
<ul
className="univer-m-0 univer-list-none univer-p-0 univer-text-sm"
style={{ fontFamily: value }}
>
{fonts.map((font) => (
<li key={font.value}>
<button
className={`
univer-flex univer-h-7 univer-w-full univer-appearance-none univer-items-center
univer-justify-between univer-gap-6 univer-rounded univer-border-none univer-bg-transparent
univer-px-2
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
`}
style={{
fontFamily: font.value,
}}
type="button"
onClick={() => handleSelectFont(font.value)}
>
{localeService.t(font.label)}
{!fontService.isFontSupported(font.value) && (
<Tooltip title={localeService.t('fontFamily.not-supported')}>
<InfoIcon
className={`
univer-text-gray-300
dark:univer-text-gray-700
`}
/>
</Tooltip>
)}
</button>
</li>
))}
</ul>
);
};
@@ -16,4 +16,3 @@
export { FontFamily } from './FontFamily';
export { FontFamilyItem } from './FontFamilyItem';
export { FONT_FAMILY_LIST } from './interface';
@@ -22,66 +22,4 @@ export interface IFontFamilyProps extends ICustomComponentProps<string> {
export const FONT_FAMILY_COMPONENT = 'UI_FONT_FAMILY_COMPONENT';
export interface IFontFamilyItemProps extends ICustomComponentProps<string> {
value: string;
}
export const FONT_FAMILY_ITEM_COMPONENT = 'UI_FONT_FAMILY_ITEM_COMPONENT';
export const FONT_FAMILY_LIST = [
{
value: 'Arial',
},
{
value: 'Times New Roman',
},
{
value: 'Tahoma',
},
{
value: 'Verdana',
},
{
value: 'Microsoft YaHei',
},
{
value: 'SimSun',
},
{
value: 'SimHei',
},
{
value: 'Kaiti',
},
{
value: 'FangSong',
},
{
value: 'NSimSun',
},
{
value: 'STXinwei',
},
{
value: 'STXingkai',
},
{
value: 'STLiti',
},
// The following 3 fonts do not work, temporarily delete
// {
// label: 'fontFamily.HanaleiFill',
// style: { 'font-family': 'HanaleiFill' },
// value: 'HanaleiFill',
// },
// {
// label: 'fontFamily.Anton',
// style: { 'font-family': 'Anton' },
// value: 'Anton',
// },
// {
// label: 'fontFamily.Pacifico',
// style: { 'font-family': 'Pacifico' },
// value: 'Pacifico',
// },
];
@@ -15,6 +15,7 @@
*/
import type { DependencyOverride } from '@univerjs/core';
import type { IFontConfig } from '../services/font.service';
import type { MenuConfig } from '../services/menu/menu';
import type { IWorkbenchOptions } from './ui/ui.controller';
@@ -30,6 +31,11 @@ export interface IUniverUIConfig extends IWorkbenchOptions {
menu?: MenuConfig;
/**
* Custom font family list to be added to the font service.
*/
customFontFamily?: IFontConfig[];
popupRootId?: string;
/**
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { IFontConfig } from '../../services/font.service';
import { createIdentifier } from '@univerjs/core';
export type RibbonType = 'default' | 'simple' | 'classic';
@@ -31,8 +32,16 @@ export interface IWorkbenchOptions {
*/
toolbar?: boolean;
/**
* The type of ribbon to be used in the UI.
*/
ribbonType?: RibbonType;
/**
* Custom font family list to be added to the font service.
*/
customFontFamily?: IFontConfig[];
/**
* If Univer should make the footer bar visible.
*/
+33 -2
View File
@@ -16,11 +16,11 @@
import type { IDisposable } from '@univerjs/core';
import type { IMessageProps } from '@univerjs/design';
import type { BuiltInUIPart, ComponentType, IComponentOptions, IDialogPartMethodOptions, ISidebarMethodOptions } from '@univerjs/ui';
import type { BuiltInUIPart, ComponentType, IComponentOptions, IDialogPartMethodOptions, IFontConfig, ISidebarMethodOptions } from '@univerjs/ui';
import type { IFacadeMenuItem, IFacadeSubmenuItem } from './f-menu-builder';
import { FUniver } from '@univerjs/core/facade';
import { IRenderManagerService } from '@univerjs/engine-render';
import { ComponentManager, connectInjector, CopyCommand, IDialogService, IMessageService, ISidebarService, IUIPartsService, PasteCommand } from '@univerjs/ui';
import { ComponentManager, connectInjector, CopyCommand, IDialogService, IFontService, IMessageService, ISidebarService, IUIPartsService, PasteCommand } from '@univerjs/ui';
import { FMenu, FSubmenu } from './f-menu-builder';
import { FShortcut } from './f-shortcut';
@@ -418,6 +418,27 @@ export interface IFUniverUIMixin {
* This will render `unit2` in the workbench's main area.
*/
setCurrent(unitId: string): void;
/**
* Append custom fonts to the font list.
* @param fonts The array of font configurations to add.
* @example
* ```ts
* univerAPI.addFonts([
* {
* value: 'CustomFont1',
* label: 'Custom Font 1',
* category: 'sans-serif',
* },
* {
* value: 'CustomFont2',
* label: 'Custom Font 2',
* category: 'serif',
* },
* ]);
* ```
*/
addFonts(fonts: IFontConfig[]): void;
}
/**
@@ -508,6 +529,16 @@ export class FUniverUIMixin extends FUniver implements IFUniverUIMixin {
this._univerInstanceService.setCurrentUnitForType(unitId);
}
override addFonts(fonts: IFontConfig[]): void {
const fontService = this._injector.get(IFontService);
fonts.forEach((font) => {
fontService.addFont({
...font,
isCustom: true,
});
});
}
}
FUniver.extend(FUniverUIMixin);
+2 -1
View File
@@ -24,7 +24,7 @@ export * from './components';
export { COLOR_PICKER_COMPONENT } from './components/color-picker/interface';
export { FontFamily } from './components/font-family/FontFamily';
export { FontFamilyItem } from './components/font-family/FontFamilyItem';
export { FONT_FAMILY_COMPONENT, FONT_FAMILY_ITEM_COMPONENT, FONT_FAMILY_LIST, type IFontFamilyItemProps, type IFontFamilyProps } from './components/font-family/interface';
export { FONT_FAMILY_COMPONENT, FONT_FAMILY_ITEM_COMPONENT, type IFontFamilyProps } from './components/font-family/interface';
export { FontSize } from './components/font-size/FontSize';
export { FONT_SIZE_COMPONENT, FONT_SIZE_LIST, HEADING_LIST } from './components/font-size/interface';
export * from './components/hooks';
@@ -71,6 +71,7 @@ export { ContextMenuService, type IContextMenuHandler, IContextMenuService } fro
export { DesktopDialogService } from './services/dialog/desktop-dialog.service';
export { IDialogService } from './services/dialog/dialog.service';
export { CanvasFloatDomService, type IFloatDom, type IFloatDomLayout } from './services/dom/canvas-dom-layer.service';
export { type IFontConfig, IFontService } from './services/font.service';
export { DesktopGalleryService } from './services/gallery/desktop-gallery.service';
export { IGalleryService } from './services/gallery/gallery.service';
export { DesktopGlobalZoneService } from './services/global-zone/desktop-global-zone.service';
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'Més',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'No s\'ha trobat aquesta font al sistema, s\'utilitza la font per defecte.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Dreceres',
+14 -16
View File
@@ -45,22 +45,20 @@ const locale = {
more: 'More',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'No such font found in the system, using default font.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Shortcuts',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'Más',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'No se encontró esta fuente en el sistema, se utiliza la fuente predeterminada.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Atajos',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'بیشتر',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'هیچ فونتی با این نام در سیستم یافت نشد، از فونت پیش‌فرض استفاده می‌شود.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'کلیدهای میانبر',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'Plus',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'Aucune police de ce type trouvée dans le système, utilisation de la police par défaut.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Raccourcis',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'もっと見る',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'システムにそのようなフォントが見つからないため、デフォルトのフォントを使用しています。',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': '微軟雅黒',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'ショートカット',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: '더 보기',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': '시스템에 해당 폰트가 없어 기본 폰트를 사용합니다.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: '단축키',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'Больше',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'В системе не найден такой шрифт, используется шрифт по умолчанию.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Сочетания клавиш',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: 'Thêm',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: 'Microsoft YaHei',
SimSun: 'SimSun',
SimHei: 'SimHei',
Kaiti: 'Kaiti',
FangSong: 'FangSong',
NSimSun: 'NSimSun',
STXinwei: 'STXinwei',
STXingkai: 'STXingkai',
STLiti: 'STLiti',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': 'Không tìm thấy phông chữ này trong hệ thống, sử dụng phông chữ mặc định.',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': 'Microsoft YaHei',
simsun: 'SimSun',
simhei: 'SimHei',
kaiti: 'Kaiti',
fangsong: 'FangSong',
nsimsun: 'NSimSun',
stxinwei: 'STXinwei',
stxingkai: 'STXingkai',
stliti: 'STLiti',
},
'shortcut-panel': {
title: 'Bảng phím tắt',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: '更多',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: '微软雅黑',
SimSun: '宋体',
SimHei: '体',
Kaiti: '体',
FangSong: '仿宋',
NSimSun: '新宋体',
STXinwei: '华文新魏',
STXingkai: '华文行楷',
STLiti: '华文隶书',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': '系统中未找到该字体,使用默认字体。',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': '微软雅黑',
simsun: '体',
simhei: '体',
kaiti: '楷体',
fangsong: '仿宋',
nsimsun: '新宋体',
stxinwei: '华文新魏',
stxingkai: '华文行楷',
stliti: '华文隶书',
},
'shortcut-panel': {
title: '快捷键面板',
+14 -16
View File
@@ -47,22 +47,20 @@ const locale: typeof enUS = {
more: '更多',
},
fontFamily: {
TimesNewRoman: 'Times New Roman',
Arial: 'Arial',
Tahoma: 'Tahoma',
Verdana: 'Verdana',
MicrosoftYaHei: '微軟雅黑',
SimSun: '宋體',
SimHei: '體',
Kaiti: '體',
FangSong: '仿宋',
NSimSun: '新宋體',
STXinwei: '華文新魏',
STXingkai: '華文行楷',
STLiti: '華文隸書',
HanaleiFill: 'HanaleiFill',
Anton: 'Anton',
Pacifico: 'Pacifico',
'not-supported': '系統中未找到該字體,使用預設字體。',
arial: 'Arial',
'times-new-roman': 'Times New Roman',
tahoma: 'Tahoma',
verdana: 'Verdana',
'microsoft-yahei': '微軟雅黑',
simsun: '體',
simhei: '體',
kaiti: '楷體',
fangsong: '仿宋',
nsimsun: '新宋體',
stxinwei: '華文新魏',
stxingkai: '華文行楷',
stliti: '華文隸書',
},
'shortcut-panel': {
title: '快捷鍵面板',
+2
View File
@@ -33,6 +33,7 @@ import { ContextMenuService, IContextMenuService } from './services/contextmenu/
import { DesktopDialogService } from './services/dialog/desktop-dialog.service';
import { IDialogService } from './services/dialog/dialog.service';
import { CanvasFloatDomService } from './services/dom/canvas-dom-layer.service';
import { FontService, IFontService } from './services/font.service';
import { DesktopGalleryService } from './services/gallery/desktop-gallery.service';
import { IGalleryService } from './services/gallery/gallery.service';
import { DesktopGlobalZoneService } from './services/global-zone/desktop-global-zone.service';
@@ -119,6 +120,7 @@ export class UniverUIPlugin extends Plugin {
[IBeforeCloseService, { useClass: DesktopBeforeCloseService }],
[ILocalFileService, { useClass: DesktopLocalFileService }],
[ICanvasPopupService, { useClass: CanvasPopupService }],
[IFontService, { useClass: FontService }],
[CanvasFloatDomService],
[IUIController, {
useFactory: (injector: Injector) => injector.createInstance(DesktopUIController, this._config),
+255
View File
@@ -0,0 +1,255 @@
/**
* 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 { IDisposable } from '@univerjs/core';
import type { IUniverUIConfig } from '../controllers/config.schema';
import { createIdentifier, IConfigService } from '@univerjs/core';
import { BehaviorSubject } from 'rxjs';
import { UI_PLUGIN_CONFIG_KEY } from '../controllers/config.schema';
export const IFontService = createIdentifier<IFontService>('univer.font-service');
/**
* Font configuration interface
*/
export interface IFontConfig {
/**
* Unique identifier, usually also the preferred value for CSS font-family
* @example 'Microsoft YaHei'
*/
value: string;
/**
* Translation key for i18n
* @example 'font.microsoft_yahei'
*/
label: string;
/**
* Font category for UI grouping (optional)
*/
category?: 'sans-serif' | 'serif' | 'monospace' | 'display' | 'handwriting';
/**
* Mark whether the font is user-added
* (Used to distinguish built-in fonts to prevent accidental deletion of core fonts)
*/
isCustom?: boolean;
}
const DEFAULT_FONT_LIST: IFontConfig[] = [{
value: 'Arial',
label: 'fontFamily.arial',
category: 'sans-serif',
}, {
value: 'Times New Roman',
label: 'fontFamily.times-new-roman',
category: 'serif',
}, {
value: 'Tahoma',
label: 'fontFamily.tahoma',
category: 'sans-serif',
}, {
value: 'Verdana',
label: 'fontFamily.verdana',
category: 'sans-serif',
}, {
value: 'Microsoft YaHei',
label: 'fontFamily.microsoft-yahei',
category: 'sans-serif',
}, {
value: 'SimSun',
label: 'fontFamily.simsun',
category: 'serif',
}, {
value: 'SimHei',
label: 'fontFamily.simhei',
category: 'sans-serif',
}, {
value: 'Kaiti',
label: 'fontFamily.kaiti',
category: 'serif',
}, {
value: 'FangSong',
label: 'fontFamily.fangsong',
category: 'serif',
}, {
value: 'NSimSun',
label: 'fontFamily.nsimsun',
category: 'serif',
}, {
value: 'STXinwei',
label: 'fontFamily.stxinwei',
category: 'serif',
}, {
value: 'STXingkai',
label: 'fontFamily.stxingkai',
category: 'serif',
}, {
value: 'STLiti',
label: 'fontFamily.stliti',
category: 'serif',
}];
export interface IFontService {
/**
* The data stream of the font list
* UI components should subscribe to this stream to render dropdown lists
* When the list changes (add, delete, update), a new value is automatically emitted
*/
readonly fonts$: BehaviorSubject<IFontConfig[]>;
/**
* Get a snapshot of the current font list (synchronously)
* Suitable for scenarios where subscribing to the stream is not needed,
* or when obtaining the current state during logic processing
*/
getFonts(): IFontConfig[];
/**
* Get a single font configuration by value
*/
getFontByValue(value: string): IFontConfig | undefined;
/**
* Check if the current browser environment supports the font
* (Based on document.fonts.check or Canvas fallback)
*/
isFontSupported(fontValue: string): boolean;
/**
* Add a new font
* @throws Error if the font value already exists
*/
addFont(font: IFontConfig): void;
/**
* Update an existing font configuration
* Supports partial updates (e.g., only updating stack or label)
* @param value The unique identifier of the font to update
* @param updates The fields to update
*/
updateFont(value: string, updates: Partial<Omit<IFontConfig, 'value'>>): void;
/**
* Remove a font
* @param value The identifier of the font to remove
* @returns boolean Whether the removal was successful (e.g., built-in fonts may not be allowed to be removed)
*/
removeFont(value: string): boolean;
/**
* Reset to the default built-in font list
* (Used for the "Restore Defaults" feature)
*/
resetToDefaults(): void;
}
export class FontService implements IFontService, IDisposable {
readonly fonts$: BehaviorSubject<IFontConfig[]> = new BehaviorSubject<IFontConfig[]>([...DEFAULT_FONT_LIST]);
constructor(@IConfigService protected readonly _configService: IConfigService) {
const config = this._configService.getConfig<IUniverUIConfig>(UI_PLUGIN_CONFIG_KEY);
const { customFontFamily = [] } = config ?? {};
if (customFontFamily.length) {
customFontFamily.forEach((font) => this.addFont({
...font,
isCustom: true,
}));
}
}
dispose() {
this.resetToDefaults();
this.fonts$.complete();
}
getFonts(): IFontConfig[] {
return this.fonts$.getValue();
}
getFontByValue(value: string): IFontConfig | undefined {
return this.getFonts().find((font) => font.value === value);
}
/**
* Check if the current browser environment supports the font
* @param fontValue
* @returns boolean Whether the font is supported
*/
isFontSupported(fontValue: string): boolean {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
return false;
}
const testString = 'abcdefghijklmnopqrstuvwxyz0123456789';
context.font = '12px monospace';
const baselineWidth = context.measureText(testString).width;
context.font = `12px "${fontValue}", monospace`;
const newWidth = context.measureText(testString).width;
return newWidth !== baselineWidth;
}
addFont(font: IFontConfig): void {
const existingFont = this.getFontByValue(font.value);
if (existingFont) {
throw new Error(`[FontService]: Font with value "${font.value}" already exists.`);
}
const updatedFonts = [...this.getFonts(), font].sort();
this.fonts$.next(updatedFonts);
}
updateFont(value: string, updates: Partial<Omit<IFontConfig, 'value'>>): void {
const fonts = this.getFonts();
const fontIndex = fonts.findIndex((font) => font.value === value);
if (fontIndex === -1) {
throw new Error(`Font with value "${value}" not found.`);
}
const updatedFont = { ...fonts[fontIndex], ...updates };
const updatedFonts = [...fonts];
updatedFonts[fontIndex] = updatedFont;
this.fonts$.next(updatedFonts);
}
removeFont(value: string): boolean {
const fonts = this.getFonts();
const fontIndex = fonts.findIndex((font) => font.value === value);
if (fontIndex === -1) {
return false;
}
const fontToRemove = fonts[fontIndex];
if (!fontToRemove.isCustom) {
// Prevent removal of built-in fonts
return false;
}
const updatedFonts = fonts.filter((font) => font.value !== value);
this.fonts$.next(updatedFonts);
return true;
}
resetToDefaults(): void {
this.fonts$.next([...DEFAULT_FONT_LIST]);
}
}