mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import type { AliasInfo, ReferenceParams } from '@blocksuite/affine-model';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/block-std';
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import type { Disposable } from '@blocksuite/global/utils';
|
||||
import {
|
||||
AliasIcon,
|
||||
BlockLinkIcon,
|
||||
DeleteIcon,
|
||||
EdgelessIcon,
|
||||
LinkedEdgelessIcon,
|
||||
LinkedPageIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import { computed, type Signal, signal } from '@preact/signals-core';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { referenceToNode } from '../utils/reference.js';
|
||||
import { DocModeProvider } from './doc-mode-service.js';
|
||||
|
||||
export type DocDisplayMetaParams = {
|
||||
referenced?: boolean;
|
||||
params?: ReferenceParams;
|
||||
} & AliasInfo;
|
||||
|
||||
/**
|
||||
* Customize document display title and icon.
|
||||
*
|
||||
* Supports the following blocks:
|
||||
*
|
||||
* * Inline View:
|
||||
* `AffineReference`
|
||||
* * Card View:
|
||||
* `EmbedLinkedDocBlockComponent`
|
||||
* `EmbedEdgelessLinkedDocBlockComponent`
|
||||
* * Embed View:
|
||||
* `EmbedSyncedDocBlockComponent`
|
||||
* `EmbedEdgelessSyncedDocBlockComponent`
|
||||
*/
|
||||
export interface DocDisplayMetaExtension {
|
||||
icon: (
|
||||
docId: string,
|
||||
referenceInfo?: DocDisplayMetaParams
|
||||
) => Signal<TemplateResult>;
|
||||
title: (
|
||||
docId: string,
|
||||
referenceInfo?: DocDisplayMetaParams
|
||||
) => Signal<string>;
|
||||
}
|
||||
|
||||
export const DocDisplayMetaProvider = createIdentifier<DocDisplayMetaExtension>(
|
||||
'DocDisplayMetaService'
|
||||
);
|
||||
|
||||
export class DocDisplayMetaService
|
||||
extends LifeCycleWatcher
|
||||
implements DocDisplayMetaExtension
|
||||
{
|
||||
static icons = {
|
||||
deleted: iconBuilder(DeleteIcon),
|
||||
aliased: iconBuilder(AliasIcon),
|
||||
page: iconBuilder(PageIcon),
|
||||
edgeless: iconBuilder(EdgelessIcon),
|
||||
linkedBlock: iconBuilder(BlockLinkIcon),
|
||||
linkedPage: iconBuilder(LinkedPageIcon),
|
||||
linkedEdgeless: iconBuilder(LinkedEdgelessIcon),
|
||||
} as const;
|
||||
|
||||
static override key = 'doc-display-meta';
|
||||
|
||||
readonly disposables: Disposable[] = [];
|
||||
|
||||
readonly iconMap = new WeakMap<Doc, Signal<TemplateResult>>();
|
||||
|
||||
readonly titleMap = new WeakMap<Doc, Signal<string>>();
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(DocDisplayMetaProvider, this, [StdIdentifier]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
this.disposables.pop()?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
icon(
|
||||
pageId: string,
|
||||
{ params, title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<TemplateResult> {
|
||||
const doc = this.std.collection.getDoc(pageId);
|
||||
|
||||
if (!doc) {
|
||||
return signal(DocDisplayMetaService.icons.deleted);
|
||||
}
|
||||
|
||||
let icon$ = this.iconMap.get(doc);
|
||||
|
||||
if (!icon$) {
|
||||
icon$ = signal(
|
||||
this.std.get(DocModeProvider).getPrimaryMode(pageId) === 'edgeless'
|
||||
? DocDisplayMetaService.icons.edgeless
|
||||
: DocDisplayMetaService.icons.page
|
||||
);
|
||||
|
||||
const disposable = this.std
|
||||
.get(DocModeProvider)
|
||||
.onPrimaryModeChange(mode => {
|
||||
icon$!.value =
|
||||
mode === 'edgeless'
|
||||
? DocDisplayMetaService.icons.edgeless
|
||||
: DocDisplayMetaService.icons.page;
|
||||
}, pageId);
|
||||
|
||||
this.disposables.push(disposable);
|
||||
this.disposables.push(
|
||||
this.std.collection.slots.docRemoved
|
||||
.filter(docId => docId === doc.id)
|
||||
.once(() => {
|
||||
const index = this.disposables.findIndex(d => d === disposable);
|
||||
if (index !== -1) {
|
||||
this.disposables.splice(index, 1);
|
||||
disposable.dispose();
|
||||
}
|
||||
this.iconMap.delete(doc);
|
||||
})
|
||||
);
|
||||
this.iconMap.set(doc, icon$);
|
||||
}
|
||||
|
||||
return computed(() => {
|
||||
if (title) {
|
||||
return DocDisplayMetaService.icons.aliased;
|
||||
}
|
||||
|
||||
if (referenceToNode({ pageId, params })) {
|
||||
return DocDisplayMetaService.icons.linkedBlock;
|
||||
}
|
||||
|
||||
if (referenced) {
|
||||
const mode =
|
||||
params?.mode ??
|
||||
this.std.get(DocModeProvider).getPrimaryMode(pageId) ??
|
||||
'page';
|
||||
return mode === 'edgeless'
|
||||
? DocDisplayMetaService.icons.linkedEdgeless
|
||||
: DocDisplayMetaService.icons.linkedPage;
|
||||
}
|
||||
|
||||
return icon$.value;
|
||||
});
|
||||
}
|
||||
|
||||
title(pageId: string, { title }: DocDisplayMetaParams = {}): Signal<string> {
|
||||
const doc = this.std.collection.getDoc(pageId);
|
||||
|
||||
if (!doc) {
|
||||
return signal(title || 'Deleted doc');
|
||||
}
|
||||
|
||||
let title$ = this.titleMap.get(doc);
|
||||
if (!title$) {
|
||||
title$ = signal(doc.meta?.title || 'Untitled');
|
||||
|
||||
const disposable = this.std.collection.meta.docMetaUpdated.on(() => {
|
||||
title$!.value = doc.meta?.title || 'Untitled';
|
||||
});
|
||||
|
||||
this.disposables.push(disposable);
|
||||
this.disposables.push(
|
||||
this.std.collection.slots.docRemoved
|
||||
.filter(docId => docId === doc.id)
|
||||
.once(() => {
|
||||
const index = this.disposables.findIndex(d => d === disposable);
|
||||
if (index !== -1) {
|
||||
this.disposables.splice(index, 1);
|
||||
disposable.dispose();
|
||||
}
|
||||
this.titleMap.delete(doc);
|
||||
})
|
||||
);
|
||||
this.titleMap.set(doc, title$);
|
||||
}
|
||||
|
||||
return computed(() => {
|
||||
return title || title$.value;
|
||||
});
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function iconBuilder(
|
||||
icon: typeof PageIcon,
|
||||
size = '1.25em',
|
||||
style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;'
|
||||
) {
|
||||
return icon({ width: size, height: size, style });
|
||||
}
|
||||
108
blocksuite/affine/shared/src/services/doc-mode-service.ts
Normal file
108
blocksuite/affine/shared/src/services/doc-mode-service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { DocMode } from '@blocksuite/affine-model';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { Extension } from '@blocksuite/block-std';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import { type Disposable, noop, Slot } from '@blocksuite/global/utils';
|
||||
|
||||
const DEFAULT_MODE: DocMode = 'page';
|
||||
|
||||
export interface DocModeProvider {
|
||||
/**
|
||||
* Set the primary mode of the doc.
|
||||
* This would not affect the current editor mode.
|
||||
* If you want to switch the editor mode, use `setEditorMode` instead.
|
||||
* @param mode - The mode to set.
|
||||
* @param docId - The id of the doc.
|
||||
*/
|
||||
setPrimaryMode: (mode: DocMode, docId: string) => void;
|
||||
/**
|
||||
* Get the primary mode of the doc.
|
||||
* Normally, it would be used to query the mode of other doc.
|
||||
* @param docId - The id of the doc.
|
||||
* @returns The primary mode of the document.
|
||||
*/
|
||||
getPrimaryMode: (docId: string) => DocMode;
|
||||
/**
|
||||
* Toggle the primary mode of the doc.
|
||||
* @param docId - The id of the doc.
|
||||
* @returns The new primary mode of the doc.
|
||||
*/
|
||||
togglePrimaryMode: (docId: string) => DocMode;
|
||||
/**
|
||||
* Subscribe to changes in the primary mode of the doc.
|
||||
* For example:
|
||||
* Embed-linked-doc-block will subscribe to the primary mode of the linked doc,
|
||||
* and will display different UI according to the primary mode of the linked doc.
|
||||
* @param handler - The handler to call when the primary mode of certain doc changes.
|
||||
* @param docId - The id of the doc.
|
||||
* @returns A disposable to stop the subscription.
|
||||
*/
|
||||
onPrimaryModeChange: (
|
||||
handler: (mode: DocMode) => void,
|
||||
docId: string
|
||||
) => Disposable;
|
||||
/**
|
||||
* Set the editor mode. Normally, it would be used to set the mode of the current editor.
|
||||
* When patch or override the doc mode service, can pass a callback to set the editor mode.
|
||||
* @param mode - The mode to set.
|
||||
*/
|
||||
setEditorMode: (mode: DocMode) => void;
|
||||
/**
|
||||
* Get current editor mode.
|
||||
* @returns The editor mode.
|
||||
*/
|
||||
getEditorMode: () => DocMode | null;
|
||||
}
|
||||
|
||||
export const DocModeProvider = createIdentifier<DocModeProvider>(
|
||||
'AffineDocModeService'
|
||||
);
|
||||
|
||||
const modeMap = new Map<string, DocMode>();
|
||||
const slotMap = new Map<string, Slot<DocMode>>();
|
||||
|
||||
export class DocModeService extends Extension implements DocModeProvider {
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(DocModeProvider, DocModeService);
|
||||
}
|
||||
|
||||
getEditorMode(): DocMode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPrimaryMode(id: string) {
|
||||
return modeMap.get(id) ?? DEFAULT_MODE;
|
||||
}
|
||||
|
||||
onPrimaryModeChange(handler: (mode: DocMode) => void, id: string) {
|
||||
if (!slotMap.get(id)) {
|
||||
slotMap.set(id, new Slot());
|
||||
}
|
||||
return slotMap.get(id)!.on(handler);
|
||||
}
|
||||
|
||||
setEditorMode(mode: DocMode) {
|
||||
noop(mode);
|
||||
}
|
||||
|
||||
setPrimaryMode(mode: DocMode, id: string) {
|
||||
modeMap.set(id, mode);
|
||||
slotMap.get(id)?.emit(mode);
|
||||
}
|
||||
|
||||
togglePrimaryMode(id: string) {
|
||||
const mode = this.getPrimaryMode(id) === 'page' ? 'edgeless' : 'page';
|
||||
this.setPrimaryMode(mode, id);
|
||||
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
export function DocModeExtension(service: DocModeProvider): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(DocModeProvider, () => service);
|
||||
},
|
||||
};
|
||||
}
|
||||
125
blocksuite/affine/shared/src/services/drag-handle-config.ts
Normal file
125
blocksuite/affine/shared/src/services/drag-handle-config.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
type DndEventState,
|
||||
type EditorHost,
|
||||
Extension,
|
||||
type ExtensionType,
|
||||
StdIdentifier,
|
||||
} from '@blocksuite/block-std';
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import type { Point } from '@blocksuite/global/utils';
|
||||
import { Job, Slice, type SliceSnapshot } from '@blocksuite/store';
|
||||
|
||||
export type DropType = 'before' | 'after' | 'in';
|
||||
export type OnDragStartProps = {
|
||||
state: DndEventState;
|
||||
startDragging: (
|
||||
blocks: BlockComponent[],
|
||||
state: DndEventState,
|
||||
dragPreview?: HTMLElement,
|
||||
dragPreviewOffset?: Point
|
||||
) => void;
|
||||
anchorBlockId: string | null;
|
||||
editorHost: EditorHost;
|
||||
};
|
||||
|
||||
export type OnDragEndProps = {
|
||||
state: DndEventState;
|
||||
draggingElements: BlockComponent[];
|
||||
dropBlockId: string;
|
||||
dropType: DropType | null;
|
||||
dragPreview: HTMLElement;
|
||||
noteScale: number;
|
||||
editorHost: EditorHost;
|
||||
};
|
||||
|
||||
export type OnDragMoveProps = {
|
||||
state: DndEventState;
|
||||
draggingElements?: BlockComponent[];
|
||||
};
|
||||
|
||||
export type DragHandleOption = {
|
||||
flavour: string | RegExp;
|
||||
edgeless?: boolean;
|
||||
onDragStart?: (props: OnDragStartProps) => boolean;
|
||||
onDragMove?: (props: OnDragMoveProps) => boolean;
|
||||
onDragEnd?: (props: OnDragEndProps) => boolean;
|
||||
};
|
||||
|
||||
export const DragHandleConfigIdentifier = createIdentifier<DragHandleOption>(
|
||||
'AffineDragHandleIdentifier'
|
||||
);
|
||||
|
||||
export function DragHandleConfigExtension(
|
||||
option: DragHandleOption
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
const key =
|
||||
typeof option.flavour === 'string'
|
||||
? option.flavour
|
||||
: option.flavour.source;
|
||||
di.addImpl(DragHandleConfigIdentifier(key), () => option);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DndApiExtensionIdentifier = createIdentifier<DNDAPIExtension>(
|
||||
'AffineDndApiIdentifier'
|
||||
);
|
||||
|
||||
export class DNDAPIExtension extends Extension {
|
||||
mimeType = 'application/x-blocksuite-dnd';
|
||||
|
||||
constructor(readonly std: BlockStdScope) {
|
||||
super();
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.add(this, [StdIdentifier]);
|
||||
|
||||
di.addImpl(DndApiExtensionIdentifier, provider => provider.get(this));
|
||||
}
|
||||
|
||||
decodeSnapshot(data: string): SliceSnapshot {
|
||||
return JSON.parse(decodeURIComponent(data));
|
||||
}
|
||||
|
||||
encodeSnapshot(json: SliceSnapshot) {
|
||||
const snapshot = JSON.stringify(json);
|
||||
return encodeURIComponent(snapshot);
|
||||
}
|
||||
|
||||
fromEntity(options: {
|
||||
docId: string;
|
||||
flavour?: string;
|
||||
blockId?: string;
|
||||
}): SliceSnapshot | null {
|
||||
const { docId, flavour = 'affine:embed-linked-doc', blockId } = options;
|
||||
|
||||
const slice = Slice.fromModels(this.std.doc, []);
|
||||
const job = new Job({ collection: this.std.collection });
|
||||
const snapshot = job.sliceToSnapshot(slice);
|
||||
if (!snapshot) {
|
||||
console.error('Failed to convert slice to snapshot');
|
||||
return null;
|
||||
}
|
||||
const props = {
|
||||
...(blockId ? { blockId } : {}),
|
||||
pageId: docId,
|
||||
};
|
||||
return {
|
||||
...snapshot,
|
||||
content: [
|
||||
{
|
||||
id: this.std.collection.idGenerator(),
|
||||
type: 'block',
|
||||
flavour,
|
||||
props,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
211
blocksuite/affine/shared/src/services/edit-props-store.ts
Normal file
211
blocksuite/affine/shared/src/services/edit-props-store.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import {
|
||||
type DeepPartial,
|
||||
DisposableGroup,
|
||||
Slot,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { computed, type Signal, signal } from '@preact/signals-core';
|
||||
import clonedeep from 'lodash.clonedeep';
|
||||
import mergeWith from 'lodash.mergewith';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
ColorSchema,
|
||||
makeDeepOptional,
|
||||
NodePropsSchema,
|
||||
} from '../utils/index.js';
|
||||
import { EditorSettingProvider } from './editor-setting-service.js';
|
||||
|
||||
const LastPropsSchema = NodePropsSchema;
|
||||
const OptionalPropsSchema = makeDeepOptional(NodePropsSchema);
|
||||
export type LastProps = z.infer<typeof NodePropsSchema>;
|
||||
export type LastPropsKey = keyof LastProps;
|
||||
|
||||
const SessionPropsSchema = z.object({
|
||||
viewport: z.union([
|
||||
z.object({
|
||||
centerX: z.number(),
|
||||
centerY: z.number(),
|
||||
zoom: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
xywh: z.string(),
|
||||
padding: z
|
||||
.tuple([z.number(), z.number(), z.number(), z.number()])
|
||||
.optional(),
|
||||
}),
|
||||
]),
|
||||
templateCache: z.string(),
|
||||
remoteColor: z.string(),
|
||||
showBidirectional: z.boolean(),
|
||||
});
|
||||
|
||||
const LocalPropsSchema = z.object({
|
||||
presentBlackBackground: z.boolean(),
|
||||
presentFillScreen: z.boolean(),
|
||||
presentHideToolbar: z.boolean(),
|
||||
|
||||
autoHideEmbedHTMLFullScreenToolbar: z.boolean(),
|
||||
});
|
||||
|
||||
type SessionProps = z.infer<typeof SessionPropsSchema>;
|
||||
type LocalProps = z.infer<typeof LocalPropsSchema>;
|
||||
type StorageProps = SessionProps & LocalProps;
|
||||
type StoragePropsKey = keyof StorageProps;
|
||||
|
||||
function isLocalProp(key: string): key is keyof LocalProps {
|
||||
return key in LocalPropsSchema.shape;
|
||||
}
|
||||
|
||||
function isSessionProp(key: string): key is keyof SessionProps {
|
||||
return key in SessionPropsSchema.shape;
|
||||
}
|
||||
|
||||
function customizer(_target: unknown, source: unknown) {
|
||||
if (
|
||||
ColorSchema.safeParse(source).success ||
|
||||
source instanceof DocCollection.Y.Text ||
|
||||
source instanceof DocCollection.Y.Array ||
|
||||
source instanceof DocCollection.Y.Map
|
||||
) {
|
||||
return source;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export class EditPropsStore extends LifeCycleWatcher {
|
||||
static override key = 'EditPropsStore';
|
||||
|
||||
private _disposables = new DisposableGroup();
|
||||
|
||||
private innerProps$: Signal<DeepPartial<LastProps>> = signal({});
|
||||
|
||||
lastProps$: Signal<LastProps>;
|
||||
|
||||
slots = {
|
||||
storageUpdated: new Slot<{
|
||||
key: StoragePropsKey;
|
||||
value: StorageProps[StoragePropsKey];
|
||||
}>(),
|
||||
};
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const initProps: LastProps = LastPropsSchema.parse(
|
||||
Object.entries(LastPropsSchema.shape).reduce((value, [key, schema]) => {
|
||||
return {
|
||||
...value,
|
||||
[key]: schema.parse(undefined),
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
|
||||
this.lastProps$ = computed(() => {
|
||||
const editorSetting$ = this.std.getOptional(EditorSettingProvider);
|
||||
const nextProps = mergeWith(
|
||||
clonedeep(initProps),
|
||||
editorSetting$?.value,
|
||||
this.innerProps$.value,
|
||||
customizer
|
||||
);
|
||||
return LastPropsSchema.parse(nextProps);
|
||||
});
|
||||
}
|
||||
|
||||
private _getStorage<T extends StoragePropsKey>(key: T) {
|
||||
return isSessionProp(key) ? sessionStorage : localStorage;
|
||||
}
|
||||
|
||||
private _getStorageKey<T extends StoragePropsKey>(key: T) {
|
||||
const id = this.std.doc.id;
|
||||
switch (key) {
|
||||
case 'viewport':
|
||||
return 'blocksuite:' + id + ':edgelessViewport';
|
||||
case 'presentBlackBackground':
|
||||
return 'blocksuite:presentation:blackBackground';
|
||||
case 'presentFillScreen':
|
||||
return 'blocksuite:presentation:fillScreen';
|
||||
case 'presentHideToolbar':
|
||||
return 'blocksuite:presentation:hideToolbar';
|
||||
case 'templateCache':
|
||||
return 'blocksuite:' + id + ':templateTool';
|
||||
case 'remoteColor':
|
||||
return 'blocksuite:remote-color';
|
||||
case 'showBidirectional':
|
||||
return 'blocksuite:' + id + ':showBidirectional';
|
||||
case 'autoHideEmbedHTMLFullScreenToolbar':
|
||||
return 'blocksuite:embedHTML:autoHideFullScreenToolbar';
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
applyLastProps(key: LastPropsKey, props: Record<string, unknown>) {
|
||||
if (['__proto__', 'constructor', 'prototype'].includes(key)) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.DefaultRuntimeError,
|
||||
`Invalid key: ${key}`
|
||||
);
|
||||
}
|
||||
const lastProps = this.lastProps$.value[key];
|
||||
return mergeWith(clonedeep(lastProps), props, customizer);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
getStorage<T extends StoragePropsKey>(key: T) {
|
||||
try {
|
||||
const storage = this._getStorage(key);
|
||||
const value = storage.getItem(this._getStorageKey(key));
|
||||
if (!value) return null;
|
||||
if (isLocalProp(key)) {
|
||||
return LocalPropsSchema.shape[key].parse(
|
||||
JSON.parse(value)
|
||||
) as StorageProps[T];
|
||||
} else if (isSessionProp(key)) {
|
||||
return SessionPropsSchema.shape[key].parse(
|
||||
JSON.parse(value)
|
||||
) as StorageProps[T];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
recordLastProps(key: LastPropsKey, props: Partial<LastProps[LastPropsKey]>) {
|
||||
const schema = OptionalPropsSchema._def.innerType.shape[key];
|
||||
if (!schema) return;
|
||||
|
||||
const overrideProps = schema.parse(props);
|
||||
if (Object.keys(overrideProps).length === 0) return;
|
||||
|
||||
const innerProps = this.innerProps$.value;
|
||||
const nextProps = mergeWith(
|
||||
clonedeep(innerProps),
|
||||
{ [key]: overrideProps },
|
||||
customizer
|
||||
);
|
||||
this.innerProps$.value = OptionalPropsSchema.parse(nextProps);
|
||||
}
|
||||
|
||||
setStorage<T extends StoragePropsKey>(key: T, value: StorageProps[T]) {
|
||||
const oldValue = this.getStorage(key);
|
||||
this._getStorage(key).setItem(
|
||||
this._getStorageKey(key),
|
||||
JSON.stringify(value)
|
||||
);
|
||||
if (oldValue === value) return;
|
||||
this.slots.storageUpdated.emit({ key, value });
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
super.unmounted();
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { DeepPartial } from '@blocksuite/global/utils';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { NodePropsSchema } from '../utils/index.js';
|
||||
|
||||
export const EditorSettingSchema = NodePropsSchema;
|
||||
|
||||
export type EditorSetting = z.infer<typeof EditorSettingSchema>;
|
||||
|
||||
export const EditorSettingProvider = createIdentifier<
|
||||
Signal<DeepPartial<EditorSetting>>
|
||||
>('AffineEditorSettingProvider');
|
||||
|
||||
export function EditorSettingExtension(
|
||||
signal: Signal<DeepPartial<EditorSetting>>
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(EditorSettingProvider, () => signal);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { EmbedCardStyle } from '@blocksuite/affine-model';
|
||||
import { Extension } from '@blocksuite/block-std';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
export type EmbedOptions = {
|
||||
flavour: string;
|
||||
urlRegex: RegExp;
|
||||
styles: EmbedCardStyle[];
|
||||
viewType: 'card' | 'embed';
|
||||
};
|
||||
|
||||
export interface EmbedOptionProvider {
|
||||
getEmbedBlockOptions(url: string): EmbedOptions | null;
|
||||
registerEmbedBlockOptions(options: EmbedOptions): void;
|
||||
}
|
||||
|
||||
export const EmbedOptionProvider = createIdentifier<EmbedOptionProvider>(
|
||||
'AffineEmbedOptionProvider'
|
||||
);
|
||||
|
||||
export class EmbedOptionService
|
||||
extends Extension
|
||||
implements EmbedOptionProvider
|
||||
{
|
||||
private _embedBlockRegistry = new Set<EmbedOptions>();
|
||||
|
||||
getEmbedBlockOptions = (url: string): EmbedOptions | null => {
|
||||
const entries = this._embedBlockRegistry.entries();
|
||||
for (const [options] of entries) {
|
||||
const regex = options.urlRegex;
|
||||
if (regex.test(url)) return options;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
registerEmbedBlockOptions = (options: EmbedOptions): void => {
|
||||
this._embedBlockRegistry.add(options);
|
||||
};
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(EmbedOptionProvider, EmbedOptionService);
|
||||
}
|
||||
}
|
||||
376
blocksuite/affine/shared/src/services/font-loader/config.ts
Normal file
376
blocksuite/affine/shared/src/services/font-loader/config.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { FontFamily, FontStyle, FontWeight } from '@blocksuite/affine-model';
|
||||
|
||||
export interface FontConfig {
|
||||
font: string;
|
||||
weight: string;
|
||||
url: string;
|
||||
style: string;
|
||||
}
|
||||
|
||||
export const AffineCanvasTextFonts: FontConfig[] = [
|
||||
// Inter, https://fonts.cdnfonts.com/css/inter?styles=29139,29134,29135,29136,29140,29141
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-Light-BETA.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-SemiBold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-LightItalic-BETA.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Kalam, https://fonts.cdnfonts.com/css/kalam?styles=15166,170689,170687
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://cdn.affine.pro/fonts/Kalam-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://cdn.affine.pro/fonts/Kalam-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://cdn.affine.pro/fonts/Kalam-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
// Satoshi, https://fonts.cdnfonts.com/css/satoshi?styles=135009,135004,135005,135006,135002,135003
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-LightItalic.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://cdn.affine.pro/fonts/Satoshi-BoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Poppins, https://fonts.cdnfonts.com/css/poppins?styles=20394,20389,20390,20391,20395,20396
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-Medium.woff',
|
||||
weight: FontWeight.Medium,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-LightItalic.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Lora, https://fonts.cdnfonts.com/css/lora-4?styles=50357,50356,50354,50355
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://cdn.affine.pro/fonts/Lora-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://cdn.affine.pro/fonts/Lora-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://cdn.affine.pro/fonts/Lora-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://cdn.affine.pro/fonts/Lora-BoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// BebasNeue, https://fonts.cdnfonts.com/css/bebas-neue?styles=169713,17622,17620
|
||||
{
|
||||
font: FontFamily.BebasNeue,
|
||||
url: 'https://cdn.affine.pro/fonts/BebasNeue-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.BebasNeue,
|
||||
url: 'https://cdn.affine.pro/fonts/BebasNeue-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
// OrelegaOne, https://fonts.cdnfonts.com/css/orelega-one?styles=148618
|
||||
{
|
||||
font: FontFamily.OrelegaOne,
|
||||
url: 'https://cdn.affine.pro/fonts/OrelegaOne-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
];
|
||||
|
||||
export const CommunityCanvasTextFonts: FontConfig[] = [
|
||||
// Inter, https://fonts.cdnfonts.com/css/inter?styles=29139,29134,29135,29136,29140,29141
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-Light-BETA.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-SemiBold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-LightItalic-BETA.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Inter,
|
||||
url: 'https://fonts.cdnfonts.com/s/19795/Inter-SemiBoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Kalam, https://fonts.cdnfonts.com/css/kalam?styles=15166,170689,170687
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://fonts.cdnfonts.com/s/13130/Kalam-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://fonts.cdnfonts.com/s/13130/Kalam-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Kalam,
|
||||
url: 'https://fonts.cdnfonts.com/s/13130/Kalam-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
// Satoshi, https://fonts.cdnfonts.com/css/satoshi?styles=135009,135004,135005,135006,135002,135003
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-LightItalic.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Satoshi,
|
||||
url: 'https://fonts.cdnfonts.com/s/85546/Satoshi-BoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Poppins, https://fonts.cdnfonts.com/css/poppins?styles=20394,20389,20390,20391,20395,20396
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-Medium.woff',
|
||||
weight: FontWeight.Medium,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-SemiBold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-LightItalic.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Poppins,
|
||||
url: 'https://fonts.cdnfonts.com/s/16009/Poppins-SemiBoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// Lora, https://fonts.cdnfonts.com/css/lora-4?styles=50357,50356,50354,50355
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://fonts.cdnfonts.com/s/29883/Lora-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://fonts.cdnfonts.com/s/29883/Lora-Bold.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://fonts.cdnfonts.com/s/29883/Lora-Italic.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
{
|
||||
font: FontFamily.Lora,
|
||||
url: 'https://fonts.cdnfonts.com/s/29883/Lora-BoldItalic.woff',
|
||||
weight: FontWeight.SemiBold,
|
||||
style: FontStyle.Italic,
|
||||
},
|
||||
// BebasNeue, https://fonts.cdnfonts.com/css/bebas-neue?styles=169713,17622,17620
|
||||
{
|
||||
font: FontFamily.BebasNeue,
|
||||
url: 'https://fonts.cdnfonts.com/s/14902/BebasNeue%20Light.woff',
|
||||
weight: FontWeight.Light,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
font: FontFamily.BebasNeue,
|
||||
url: 'https://fonts.cdnfonts.com/s/14902/BebasNeue-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
// OrelegaOne, https://fonts.cdnfonts.com/css/orelega-one?styles=148618
|
||||
{
|
||||
font: FontFamily.OrelegaOne,
|
||||
url: 'https://fonts.cdnfonts.com/s/93179/OrelegaOne-Regular.woff',
|
||||
weight: FontWeight.Regular,
|
||||
style: FontStyle.Normal,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
import { type ExtensionType, LifeCycleWatcher } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import { IS_FIREFOX } from '@blocksuite/global/env';
|
||||
|
||||
import type { FontConfig } from './config.js';
|
||||
|
||||
const initFontFace = IS_FIREFOX
|
||||
? ({ font, weight, url, style }: FontConfig) =>
|
||||
new FontFace(`"${font}"`, `url(${url})`, {
|
||||
weight,
|
||||
style,
|
||||
})
|
||||
: ({ font, weight, url, style }: FontConfig) =>
|
||||
new FontFace(font, `url(${url})`, {
|
||||
weight,
|
||||
style,
|
||||
});
|
||||
|
||||
export class FontLoaderService extends LifeCycleWatcher {
|
||||
static override readonly key = 'font-loader';
|
||||
|
||||
readonly fontFaces: FontFace[] = [];
|
||||
|
||||
get ready() {
|
||||
return Promise.all(this.fontFaces.map(fontFace => fontFace.loaded));
|
||||
}
|
||||
|
||||
load(fonts: FontConfig[]) {
|
||||
this.fontFaces.push(
|
||||
...fonts.map(font => {
|
||||
const fontFace = initFontFace(font);
|
||||
document.fonts.add(fontFace);
|
||||
fontFace.load().catch(console.error);
|
||||
return fontFace;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
const config = this.std.getOptional(FontConfigIdentifier);
|
||||
if (config) {
|
||||
this.load(config);
|
||||
}
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.fontFaces.forEach(fontFace => document.fonts.delete(fontFace));
|
||||
this.fontFaces.splice(0, this.fontFaces.length);
|
||||
}
|
||||
}
|
||||
|
||||
export const FontConfigIdentifier =
|
||||
createIdentifier<FontConfig[]>('AffineFontConfig');
|
||||
|
||||
export const FontConfigExtension = (
|
||||
fontConfig: FontConfig[]
|
||||
): ExtensionType => ({
|
||||
setup: di => {
|
||||
di.addImpl(FontConfigIdentifier, () => fontConfig);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './config.js';
|
||||
export * from './font-loader-service.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
export interface GenerateDocUrlService {
|
||||
generateDocUrl: (docId: string, params?: ReferenceParams) => string | void;
|
||||
}
|
||||
|
||||
export const GenerateDocUrlProvider = createIdentifier<GenerateDocUrlService>(
|
||||
'GenerateDocUrlService'
|
||||
);
|
||||
|
||||
export function GenerateDocUrlExtension(
|
||||
generateDocUrlProvider: GenerateDocUrlService
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(GenerateDocUrlProvider, generateDocUrlProvider);
|
||||
},
|
||||
};
|
||||
}
|
||||
13
blocksuite/affine/shared/src/services/index.ts
Normal file
13
blocksuite/affine/shared/src/services/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './doc-display-meta-service.js';
|
||||
export * from './doc-mode-service.js';
|
||||
export * from './drag-handle-config.js';
|
||||
export * from './edit-props-store.js';
|
||||
export * from './editor-setting-service.js';
|
||||
export * from './embed-option-service.js';
|
||||
export * from './font-loader/index.js';
|
||||
export * from './generate-url-service.js';
|
||||
export * from './notification-service.js';
|
||||
export * from './parse-url-service.js';
|
||||
export * from './quick-search-service.js';
|
||||
export * from './telemetry-service/index.js';
|
||||
export * from './theme-service.js';
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export interface NotificationService {
|
||||
toast(
|
||||
message: string,
|
||||
options?: {
|
||||
duration?: number;
|
||||
portal?: HTMLElement;
|
||||
}
|
||||
): void;
|
||||
confirm(options: {
|
||||
title: string | TemplateResult;
|
||||
message: string | TemplateResult;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
abort?: AbortSignal;
|
||||
}): Promise<boolean>;
|
||||
prompt(options: {
|
||||
title: string | TemplateResult;
|
||||
message: string | TemplateResult;
|
||||
autofill?: string;
|
||||
placeholder?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
abort?: AbortSignal;
|
||||
}): Promise<string | null>; // when cancel, return null
|
||||
notify(options: {
|
||||
title: string | TemplateResult;
|
||||
message?: string | TemplateResult;
|
||||
accent?: 'info' | 'success' | 'warning' | 'error';
|
||||
duration?: number; // unit ms, give 0 to disable auto dismiss
|
||||
abort?: AbortSignal;
|
||||
action?: {
|
||||
label: string | TemplateResult;
|
||||
onClick: () => void;
|
||||
};
|
||||
onClose: () => void;
|
||||
}): void;
|
||||
}
|
||||
|
||||
export const NotificationProvider = createIdentifier<NotificationService>(
|
||||
'AffineNotificationService'
|
||||
);
|
||||
|
||||
export function NotificationExtension(
|
||||
notificationService: NotificationService
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(NotificationProvider, notificationService);
|
||||
},
|
||||
};
|
||||
}
|
||||
22
blocksuite/affine/shared/src/services/parse-url-service.ts
Normal file
22
blocksuite/affine/shared/src/services/parse-url-service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
export interface ParseDocUrlService {
|
||||
parseDocUrl: (
|
||||
url: string
|
||||
) => ({ docId: string } & ReferenceParams) | undefined;
|
||||
}
|
||||
|
||||
export const ParseDocUrlProvider =
|
||||
createIdentifier<ParseDocUrlService>('ParseDocUrlService');
|
||||
|
||||
export function ParseDocUrlExtension(
|
||||
parseDocUrlService: ParseDocUrlService
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(ParseDocUrlProvider, parseDocUrlService);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
export interface QuickSearchService {
|
||||
openQuickSearch: () => Promise<QuickSearchResult>;
|
||||
}
|
||||
|
||||
export type QuickSearchResult =
|
||||
| {
|
||||
docId: string;
|
||||
params?: ReferenceParams;
|
||||
}
|
||||
| {
|
||||
externalUrl: string;
|
||||
}
|
||||
| null;
|
||||
|
||||
export const QuickSearchProvider = createIdentifier<QuickSearchService>(
|
||||
'AffineQuickSearchService'
|
||||
);
|
||||
|
||||
export function QuickSearchExtension(
|
||||
quickSearchService: QuickSearchService
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(QuickSearchProvider, quickSearchService);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
type OrderType = 'desc' | 'asc';
|
||||
export type WithParams<Map, T> = { [K in keyof Map]: Map[K] & T };
|
||||
export type SortParams = {
|
||||
fieldId: string;
|
||||
fieldType: string;
|
||||
orderType: OrderType;
|
||||
orderIndex: number;
|
||||
};
|
||||
export type ViewParams = {
|
||||
viewId: string;
|
||||
viewType: string;
|
||||
};
|
||||
export type DatabaseParams = {
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
export type DatabaseViewEvents = {
|
||||
DatabaseSortClear: {
|
||||
rulesCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DatabaseEvents = {
|
||||
AddDatabase: {};
|
||||
};
|
||||
|
||||
export interface DatabaseAllSortEvents {
|
||||
DatabaseSortAdd: {};
|
||||
DatabaseSortRemove: {};
|
||||
DatabaseSortModify: {
|
||||
oldOrderType: OrderType;
|
||||
oldFieldType: string;
|
||||
oldFieldId: string;
|
||||
};
|
||||
DatabaseSortReorder: {
|
||||
prevFieldType: string;
|
||||
nextFieldType: string;
|
||||
newOrderIndex: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type DatabaseAllViewEvents = DatabaseViewEvents &
|
||||
WithParams<DatabaseAllSortEvents, SortParams>;
|
||||
|
||||
export type DatabaseAllEvents = DatabaseEvents &
|
||||
WithParams<DatabaseAllViewEvents, ViewParams>;
|
||||
|
||||
export type OutDatabaseAllEvents = WithParams<
|
||||
DatabaseAllEvents,
|
||||
DatabaseParams
|
||||
>;
|
||||
|
||||
export type EventTraceFn<Events> = <K extends keyof Events>(
|
||||
key: K,
|
||||
params: Events[K]
|
||||
) => void;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './database.js';
|
||||
export * from './link.js';
|
||||
export * from './telemetry-service.js';
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { TelemetryEvent } from './types.js';
|
||||
|
||||
export type LinkEventType =
|
||||
| 'CopiedLink'
|
||||
| 'OpenedAliasPopup'
|
||||
| 'SavedAlias'
|
||||
| 'ResetedAlias'
|
||||
| 'OpenedViewSelector'
|
||||
| 'SelectedView'
|
||||
| 'OpenedCaptionEditor'
|
||||
| 'OpenedCardStyleSelector'
|
||||
| 'SelectedCardStyle'
|
||||
| 'OpenedCardScaleSelector'
|
||||
| 'SelectedCardScale';
|
||||
|
||||
export type LinkToolbarEvents = Record<LinkEventType, TelemetryEvent>;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
import type { OutDatabaseAllEvents } from './database.js';
|
||||
import type { LinkToolbarEvents } from './link.js';
|
||||
import type {
|
||||
AttachmentUploadedEvent,
|
||||
DocCreatedEvent,
|
||||
ElementCreationEvent,
|
||||
ElementLockEvent,
|
||||
MindMapCollapseEvent,
|
||||
TelemetryEvent,
|
||||
} from './types.js';
|
||||
|
||||
export type TelemetryEventMap = OutDatabaseAllEvents &
|
||||
LinkToolbarEvents & {
|
||||
DocCreated: DocCreatedEvent;
|
||||
Link: TelemetryEvent;
|
||||
LinkedDocCreated: TelemetryEvent;
|
||||
SplitNote: TelemetryEvent;
|
||||
CanvasElementAdded: ElementCreationEvent;
|
||||
EdgelessElementLocked: ElementLockEvent;
|
||||
ExpandedAndCollapsed: MindMapCollapseEvent;
|
||||
AttachmentUploadedEvent: AttachmentUploadedEvent;
|
||||
};
|
||||
|
||||
export interface TelemetryService {
|
||||
track<T extends keyof TelemetryEventMap>(
|
||||
eventName: T,
|
||||
props: TelemetryEventMap[T]
|
||||
): void;
|
||||
}
|
||||
|
||||
export const TelemetryProvider = createIdentifier<TelemetryService>(
|
||||
'AffineTelemetryService'
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
export type ElementCreationSource =
|
||||
| 'shortcut'
|
||||
| 'toolbar:general'
|
||||
| 'toolbar:dnd'
|
||||
| 'canvas:drop'
|
||||
| 'canvas:draw'
|
||||
| 'canvas:dbclick'
|
||||
| 'canvas:paste'
|
||||
| 'context-menu'
|
||||
| 'ai'
|
||||
| 'internal'
|
||||
| 'conversation'
|
||||
| 'manually save';
|
||||
|
||||
export interface TelemetryEvent {
|
||||
page?: string;
|
||||
segment?: string;
|
||||
module?: string;
|
||||
control?: string;
|
||||
type?: string;
|
||||
category?: string;
|
||||
other?: unknown;
|
||||
}
|
||||
|
||||
export interface DocCreatedEvent extends TelemetryEvent {
|
||||
page?: 'doc editor' | 'whiteboard editor';
|
||||
segment?: 'whiteboard' | 'note' | 'doc';
|
||||
module?:
|
||||
| 'slash commands'
|
||||
| 'format toolbar'
|
||||
| 'edgeless toolbar'
|
||||
| 'inline @';
|
||||
category?: 'page' | 'whiteboard';
|
||||
}
|
||||
|
||||
export interface ElementCreationEvent extends TelemetryEvent {
|
||||
segment?: 'toolbar' | 'whiteboard' | 'right sidebar';
|
||||
page?: 'doc editor' | 'whiteboard editor';
|
||||
module?: 'toolbar' | 'canvas' | 'ai chat panel';
|
||||
control?: ElementCreationSource;
|
||||
}
|
||||
|
||||
export interface ElementLockEvent extends TelemetryEvent {
|
||||
page: 'whiteboard editor';
|
||||
segment: 'element toolbar';
|
||||
module: 'element toolbar';
|
||||
control: 'lock' | 'unlock' | 'group-lock';
|
||||
}
|
||||
|
||||
export interface MindMapCollapseEvent extends TelemetryEvent {
|
||||
page: 'whiteboard editor';
|
||||
segment: 'mind map';
|
||||
type: 'expand' | 'collapse';
|
||||
}
|
||||
|
||||
export interface AttachmentUploadedEvent extends TelemetryEvent {
|
||||
page: 'doc editor' | 'whiteboard editor';
|
||||
segment: 'attachment';
|
||||
module: 'attachment';
|
||||
control: 'uploader';
|
||||
type: string; // file type
|
||||
category: 'success' | 'failure';
|
||||
}
|
||||
207
blocksuite/affine/shared/src/services/theme-service.ts
Normal file
207
blocksuite/affine/shared/src/services/theme-service.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { type Color, ColorScheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
Extension,
|
||||
type ExtensionType,
|
||||
StdIdentifier,
|
||||
} from '@blocksuite/block-std';
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import {
|
||||
type AffineCssVariables,
|
||||
combinedDarkCssVariables,
|
||||
combinedLightCssVariables,
|
||||
} from '@toeverything/theme';
|
||||
|
||||
import { isInsideEdgelessEditor } from '../utils/index.js';
|
||||
|
||||
const TRANSPARENT = 'transparent';
|
||||
|
||||
export const ThemeExtensionIdentifier = createIdentifier<ThemeExtension>(
|
||||
'AffineThemeExtension'
|
||||
);
|
||||
|
||||
export interface ThemeExtension {
|
||||
getAppTheme?: () => Signal<ColorScheme>;
|
||||
getEdgelessTheme?: (docId?: string) => Signal<ColorScheme>;
|
||||
}
|
||||
|
||||
export function OverrideThemeExtension(service: ThemeExtension): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(ThemeExtensionIdentifier, () => service);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ThemeProvider = createIdentifier<ThemeService>(
|
||||
'AffineThemeProvider'
|
||||
);
|
||||
|
||||
export class ThemeService extends Extension {
|
||||
app$: Signal<ColorScheme>;
|
||||
|
||||
edgeless$: Signal<ColorScheme>;
|
||||
|
||||
get appTheme() {
|
||||
return this.app$.peek();
|
||||
}
|
||||
|
||||
get edgelessTheme() {
|
||||
return this.edgeless$.peek();
|
||||
}
|
||||
|
||||
get theme() {
|
||||
return isInsideEdgelessEditor(this.std.host)
|
||||
? this.edgelessTheme
|
||||
: this.appTheme;
|
||||
}
|
||||
|
||||
get theme$() {
|
||||
return isInsideEdgelessEditor(this.std.host) ? this.edgeless$ : this.app$;
|
||||
}
|
||||
|
||||
constructor(private std: BlockStdScope) {
|
||||
super();
|
||||
const extension = this.std.getOptional(ThemeExtensionIdentifier);
|
||||
this.app$ = extension?.getAppTheme?.() || getThemeObserver().theme$;
|
||||
this.edgeless$ =
|
||||
extension?.getEdgelessTheme?.(this.std.doc.id) ||
|
||||
getThemeObserver().theme$;
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(ThemeProvider, ThemeService, [StdIdentifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CSS's color property with `var` or `light-dark` functions.
|
||||
*
|
||||
* Sometimes used to set the frame/note background.
|
||||
*
|
||||
* @param color - A color value.
|
||||
* @param fallback - If color value processing fails, it will be used as a fallback.
|
||||
* @returns - A color property string.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* `rgba(255,0,0)`
|
||||
* `#fff`
|
||||
* `light-dark(#fff, #000)`
|
||||
* `var(--affine-palette-shape-blue)`
|
||||
* ```
|
||||
*/
|
||||
generateColorProperty(
|
||||
color: Color,
|
||||
fallback = 'transparent',
|
||||
theme = this.theme
|
||||
) {
|
||||
let result: string | undefined = undefined;
|
||||
|
||||
if (typeof color === 'object') {
|
||||
result = color[theme] ?? color.normal;
|
||||
} else {
|
||||
result = color;
|
||||
}
|
||||
if (!result) {
|
||||
result = fallback;
|
||||
}
|
||||
if (result.startsWith('--')) {
|
||||
return result.endsWith(TRANSPARENT) ? TRANSPARENT : `var(${result})`;
|
||||
}
|
||||
|
||||
return result ?? TRANSPARENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a color with the current theme.
|
||||
*
|
||||
* @param color - A color value.
|
||||
* @param fallback - If color value processing fails, it will be used as a fallback.
|
||||
* @param real - If true, it returns the computed style.
|
||||
* @returns - A color property string.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* `rgba(255,0,0)`
|
||||
* `#fff`
|
||||
* `--affine-palette-shape-blue`
|
||||
* ```
|
||||
*/
|
||||
getColorValue(
|
||||
color: Color,
|
||||
fallback = TRANSPARENT,
|
||||
real = false,
|
||||
theme = this.theme
|
||||
) {
|
||||
let result: string | undefined = undefined;
|
||||
|
||||
if (typeof color === 'object') {
|
||||
result = color[theme] ?? color.normal;
|
||||
} else {
|
||||
result = color;
|
||||
}
|
||||
if (!result) {
|
||||
result = fallback;
|
||||
}
|
||||
if (real && result.startsWith('--')) {
|
||||
result = result.endsWith(TRANSPARENT)
|
||||
? TRANSPARENT
|
||||
: this.getCssVariableColor(result, theme);
|
||||
}
|
||||
|
||||
return result ?? TRANSPARENT;
|
||||
}
|
||||
|
||||
getCssVariableColor(property: string, theme = this.theme) {
|
||||
if (property.startsWith('--')) {
|
||||
if (property.endsWith(TRANSPARENT)) {
|
||||
return TRANSPARENT;
|
||||
}
|
||||
const key = property as keyof AffineCssVariables;
|
||||
const color =
|
||||
theme === ColorScheme.Dark
|
||||
? combinedDarkCssVariables[key]
|
||||
: combinedLightCssVariables[key];
|
||||
return color;
|
||||
}
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeObserver {
|
||||
private observer: MutationObserver;
|
||||
|
||||
theme$ = signal(ColorScheme.Light);
|
||||
|
||||
constructor() {
|
||||
const COLOR_SCHEMES: string[] = Object.values(ColorScheme);
|
||||
this.observer = new MutationObserver(() => {
|
||||
const mode = document.documentElement.dataset.theme;
|
||||
if (!mode) return;
|
||||
if (!COLOR_SCHEMES.includes(mode)) return;
|
||||
if (mode === this.theme$.value) return;
|
||||
|
||||
this.theme$.value = mode as ColorScheme;
|
||||
});
|
||||
this.observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme'],
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export const getThemeObserver = (function () {
|
||||
let observer: ThemeObserver;
|
||||
return function () {
|
||||
if (observer) return observer;
|
||||
observer = new ThemeObserver();
|
||||
return observer;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user