mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
55
blocksuite/affine/components/src/peek/commands.ts
Normal file
55
blocksuite/affine/components/src/peek/commands.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/// <reference types="@blocksuite/affine-shared/commands" />
|
||||
import type {
|
||||
BlockComponent,
|
||||
Command,
|
||||
InitCommandCtx,
|
||||
} from '@blocksuite/block-std';
|
||||
|
||||
import { isPeekable, peek } from './peekable.js';
|
||||
|
||||
const getSelectedPeekableBlocks = (cmd: InitCommandCtx) => {
|
||||
const [result, ctx] = cmd.std.command
|
||||
.chain()
|
||||
.tryAll(chain => [chain.getTextSelection(), chain.getBlockSelections()])
|
||||
.getSelectedBlocks({ types: ['text', 'block'] })
|
||||
.run();
|
||||
return ((result ? ctx.selectedBlocks : []) || []).filter(isPeekable);
|
||||
};
|
||||
|
||||
export const getSelectedPeekableBlocksCommand: Command<
|
||||
'selectedBlocks',
|
||||
'selectedPeekableBlocks'
|
||||
> = (ctx, next) => {
|
||||
const selectedPeekableBlocks = getSelectedPeekableBlocks(ctx);
|
||||
if (selectedPeekableBlocks.length > 0) {
|
||||
next({ selectedPeekableBlocks });
|
||||
}
|
||||
};
|
||||
|
||||
export const peekSelectedBlockCommand: Command<'selectedBlocks'> = (
|
||||
ctx,
|
||||
next
|
||||
) => {
|
||||
const peekableBlocks = getSelectedPeekableBlocks(ctx);
|
||||
// if there are multiple blocks, peek the first one
|
||||
const block = peekableBlocks.at(0);
|
||||
|
||||
if (block) {
|
||||
peek(block);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace BlockSuite {
|
||||
interface CommandContext {
|
||||
selectedPeekableBlocks?: BlockComponent[];
|
||||
}
|
||||
|
||||
interface Commands {
|
||||
peekSelectedBlock: typeof peekSelectedBlockCommand;
|
||||
getSelectedPeekableBlocks: typeof getSelectedPeekableBlocksCommand;
|
||||
// todo: add command for peek an inline element?
|
||||
}
|
||||
}
|
||||
}
|
||||
31
blocksuite/affine/components/src/peek/controller.ts
Normal file
31
blocksuite/affine/components/src/peek/controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { PeekViewProvider } from './service.js';
|
||||
import type { PeekableClass, PeekViewService } from './type.js';
|
||||
|
||||
export class PeekableController<T extends PeekableClass> {
|
||||
private _getPeekViewService = (): PeekViewService | null => {
|
||||
return this.target.std.getOptional(PeekViewProvider);
|
||||
};
|
||||
|
||||
peek = (template?: TemplateResult) => {
|
||||
return Promise.resolve<void>(
|
||||
this._getPeekViewService()?.peek({
|
||||
target: this.target,
|
||||
template,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
get peekable() {
|
||||
return (
|
||||
!!this._getPeekViewService() &&
|
||||
(this.enable ? this.enable(this.target) : true)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private target: T,
|
||||
private enable?: (e: T) => boolean
|
||||
) {}
|
||||
}
|
||||
8
blocksuite/affine/components/src/peek/index.ts
Normal file
8
blocksuite/affine/components/src/peek/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
getSelectedPeekableBlocksCommand,
|
||||
peekSelectedBlockCommand,
|
||||
} from './commands.js';
|
||||
export { PeekableController } from './controller.js';
|
||||
export { isPeekable, peek, Peekable } from './peekable.js';
|
||||
export * from './service.js';
|
||||
export type { PeekableOptions, PeekOptions, PeekViewService } from './type.js';
|
||||
81
blocksuite/affine/components/src/peek/peekable.ts
Normal file
81
blocksuite/affine/components/src/peek/peekable.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { isInsideEdgelessEditor } from '@blocksuite/affine-shared/utils';
|
||||
import type { Constructor } from '@blocksuite/global/utils';
|
||||
import type { LitElement, TemplateResult } from 'lit';
|
||||
|
||||
import { PeekableController } from './controller.js';
|
||||
import type { PeekableClass, PeekableOptions } from './type.js';
|
||||
|
||||
const symbol = Symbol('peekable');
|
||||
|
||||
export const isPeekable = <Element extends LitElement>(e: Element): boolean => {
|
||||
return Reflect.has(e, symbol) && (e as any)[symbol]?.peekable;
|
||||
};
|
||||
|
||||
export const peek = <Element extends LitElement>(
|
||||
e: Element,
|
||||
template?: TemplateResult
|
||||
): void => {
|
||||
isPeekable(e) && (e as any)[symbol]?.peek(template);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a class as peekable, which means the class can be peeked by the peek view service.
|
||||
*
|
||||
* Note: This class must be syntactically below the `@customElement` decorator (it will be applied before customElement).
|
||||
*/
|
||||
export const Peekable =
|
||||
<T extends PeekableClass, C extends Constructor<PeekableClass>>(
|
||||
options: PeekableOptions<T> = {
|
||||
action: ['double-click', 'shift-click'],
|
||||
}
|
||||
) =>
|
||||
(Class: C, context: ClassDecoratorContext) => {
|
||||
if (context.kind !== 'class') {
|
||||
console.error('@Peekable() can only be applied to a class');
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.action === undefined)
|
||||
options.action = ['double-click', 'shift-click'];
|
||||
|
||||
const actions = Array.isArray(options.action)
|
||||
? options.action
|
||||
: options.action
|
||||
? [options.action]
|
||||
: [];
|
||||
|
||||
const derivedClass = class extends Class {
|
||||
[symbol] = new PeekableController(this as unknown as T, options.enableOn);
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const target: HTMLElement =
|
||||
(options.selector ? this.querySelector(options.selector) : this) ||
|
||||
this;
|
||||
|
||||
if (actions.includes('double-click')) {
|
||||
this.disposables.addFromEvent(target, 'dblclick', e => {
|
||||
if (this[symbol].peekable) {
|
||||
e.stopPropagation();
|
||||
this[symbol].peek().catch(console.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (
|
||||
actions.includes('shift-click') &&
|
||||
// shift click in edgeless should be selection
|
||||
!isInsideEdgelessEditor(this.std.host)
|
||||
) {
|
||||
this.disposables.addFromEvent(target, 'click', e => {
|
||||
if (e.shiftKey && this[symbol].peekable) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
this[symbol].peek().catch(console.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return derivedClass as unknown as C;
|
||||
};
|
||||
16
blocksuite/affine/components/src/peek/service.ts
Normal file
16
blocksuite/affine/components/src/peek/service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
|
||||
import type { PeekViewService } from './type.js';
|
||||
|
||||
export const PeekViewProvider = createIdentifier<PeekViewService>(
|
||||
'AffinePeekViewProvider'
|
||||
);
|
||||
|
||||
export function PeekViewExtension(service: PeekViewService): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(PeekViewProvider, () => service);
|
||||
},
|
||||
};
|
||||
}
|
||||
70
blocksuite/affine/components/src/peek/type.ts
Normal file
70
blocksuite/affine/components/src/peek/type.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { BlockComponent, BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { DisposableClass } from '@blocksuite/global/utils';
|
||||
import type { LitElement, TemplateResult } from 'lit';
|
||||
|
||||
export type PeekableClass = { std: BlockStdScope } & DisposableClass &
|
||||
LitElement;
|
||||
|
||||
export interface PeekOptions {
|
||||
/**
|
||||
* Abort signal to abort the peek view
|
||||
*/
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface PeekViewService {
|
||||
/**
|
||||
* Peek a target element page ref info
|
||||
* @param pageRef The page ref info to peek.
|
||||
* @returns A promise that resolves when the peek view is closed.
|
||||
*/
|
||||
peek(
|
||||
pageRef: {
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
databaseId?: string;
|
||||
databaseDocId?: string;
|
||||
databaseRowId?: string;
|
||||
elementIds?: string[];
|
||||
target?: HTMLElement;
|
||||
},
|
||||
options?: PeekOptions
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Peek a target element with a optional template
|
||||
* @param target The target element to peek. There are two use cases:
|
||||
* 1. If the template is not given, peek view content rendering will be delegated to the implementation of peek view service.
|
||||
* 2. To determine the origin of the peek view modal animation
|
||||
* @param template Optional template to render in the peek view modal. If not given, the peek view service will render the content.
|
||||
* @returns A promise that resolves when the peek view is closed.
|
||||
*/
|
||||
peek(
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
element: { target: HTMLElement; template?: TemplateResult },
|
||||
options?: PeekOptions
|
||||
): Promise<void>;
|
||||
|
||||
peek<Element extends BlockComponent>(
|
||||
element: { target: Element; template?: TemplateResult },
|
||||
options?: PeekOptions
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
type PeekableAction = 'double-click' | 'shift-click';
|
||||
|
||||
export type PeekableOptions<T extends PeekableClass> = {
|
||||
/**
|
||||
* Action to bind to the peekable element. default to ['double-click', 'shift-click']
|
||||
* false means do not bind any action.
|
||||
*/
|
||||
action?: PeekableAction | PeekableAction[] | false;
|
||||
/**
|
||||
* It will check the block is enable to peek or not
|
||||
*/
|
||||
enableOn?: (block: T) => boolean;
|
||||
/**
|
||||
* Selector inside of the peekable element to bind the action
|
||||
*/
|
||||
selector?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user