feat(editor): add more open doc options to editor toolbar (#9588)

fix AF-2036, AF-2092
This commit is contained in:
pengx17
2025-01-09 08:04:21 +00:00
parent 890a962196
commit f78857bb11
41 changed files with 558 additions and 271 deletions

View File

@@ -17,7 +17,6 @@ import type {
insertEmbedLinkedDocCommand,
} from './embed-linked-doc-block/commands/insert-embed-linked-doc';
import { EmbedEdgelessLinkedDocBlockComponent } from './embed-linked-doc-block/embed-edgeless-linked-doc-block';
import type { EmbedLinkedDocBlockConfig } from './embed-linked-doc-block/embed-linked-doc-config';
import {
EmbedLoomBlockComponent,
type EmbedLoomBlockService,
@@ -123,9 +122,7 @@ declare global {
interface CommandContext {
insertedLinkType?: Promise<InsertedLinkType>;
}
interface BlockConfigs {
'affine:embed-linked-doc': EmbedLinkedDocBlockConfig;
}
interface Commands {
insertEmbedLinkedDoc: typeof insertEmbedLinkedDocCommand;
}

View File

@@ -4,7 +4,11 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { cloneReferenceInfoWithoutAliases } from '@blocksuite/affine-shared/utils';
import {
cloneReferenceInfoWithoutAliases,
isNewTabTrigger,
isNewViewTrigger,
} from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/utils';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
@@ -65,9 +69,10 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
}
protected override _handleClick(evt: MouseEvent): void {
if (this.config.handleClick) {
this.config.handleClick(evt, this.host, this.referenceInfo$.peek());
return;
if (isNewTabTrigger(evt)) {
this.open({ openMode: 'open-in-new-tab', event: evt });
} else if (isNewViewTrigger(evt)) {
this.open({ openMode: 'open-in-new-view', event: evt });
}
}
}

View File

@@ -14,11 +14,15 @@ import {
DocDisplayMetaProvider,
DocModeProvider,
FeatureFlagService,
OpenDocExtensionIdentifier,
type OpenDocMode,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import {
cloneReferenceInfo,
cloneReferenceInfoWithoutAliases,
isNewTabTrigger,
isNewViewTrigger,
matchFlavours,
referenceToNode,
} from '@blocksuite/affine-shared/utils';
@@ -39,10 +43,6 @@ import {
renderLinkedDocInCard,
} from '../common/render-linked-doc.js';
import { SyncedDocErrorIcon } from '../embed-synced-doc-block/styles.js';
import {
type EmbedLinkedDocBlockConfig,
EmbedLinkedDocBlockConfigIdentifier,
} from './embed-linked-doc-config.js';
import { styles } from './styles.js';
import { getEmbedLinkedDocIcons } from './utils.js';
@@ -205,10 +205,18 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
.icon(pageId, { params, title, referenced: true }).value;
});
open = () => {
this.std
.getOptional(RefNodeSlotsProvider)
?.docLinkClicked.emit(this.referenceInfo$.peek());
open = ({
openMode,
event,
}: {
openMode?: OpenDocMode;
event?: MouseEvent;
} = {}) => {
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
...this.referenceInfo$.peek(),
openMode,
event,
});
};
refreshData = () => {
@@ -228,12 +236,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
);
});
get config(): EmbedLinkedDocBlockConfig {
return (
this.std.provider.getOptional(EmbedLinkedDocBlockConfigIdentifier) || {}
);
}
get docTitle() {
return this.model.title || this.linkedDoc?.meta?.title || 'Untitled';
}
@@ -247,22 +249,16 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
}
private _handleDoubleClick(event: MouseEvent) {
if (this.config.handleDoubleClick) {
this.config.handleDoubleClick(
event,
this.host,
this.referenceInfo$.peek()
);
if (event.defaultPrevented) {
return;
}
}
if (isPeekable(this)) {
return;
}
event.stopPropagation();
this.open();
const openDocService = this.std.get(OpenDocExtensionIdentifier);
const shouldOpenInPeek =
openDocService.isAllowed('open-in-center-peek') && isPeekable(this);
this.open({
openMode: shouldOpenInPeek
? 'open-in-center-peek'
: 'open-in-active-view',
event,
});
}
private _isDocEmpty() {
@@ -274,13 +270,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
}
protected _handleClick(event: MouseEvent) {
if (this.config.handleClick) {
this.config.handleClick(event, this.host, this.referenceInfo$.peek());
if (event.defaultPrevented) {
return;
}
if (isNewTabTrigger(event)) {
this.open({ openMode: 'open-in-new-tab', event });
} else if (isNewViewTrigger(event)) {
this.open({ openMode: 'open-in-new-view', event });
}
this._selectBlock();
}

View File

@@ -1,30 +0,0 @@
import type { ReferenceInfo } from '@blocksuite/affine-model';
import type { EditorHost } from '@blocksuite/block-std';
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
export interface EmbedLinkedDocBlockConfig {
handleClick?: (
e: MouseEvent,
host: EditorHost,
referenceInfo: ReferenceInfo
) => void;
handleDoubleClick?: (
e: MouseEvent,
host: EditorHost,
referenceInfo: ReferenceInfo
) => void;
}
export const EmbedLinkedDocBlockConfigIdentifier =
createIdentifier<EmbedLinkedDocBlockConfig>('EmbedLinkedDocBlockConfig');
export function EmbedLinkedDocBlockConfigExtension(
config: EmbedLinkedDocBlockConfig
): ExtensionType {
return {
setup: di => {
di.addImpl(EmbedLinkedDocBlockConfigIdentifier, () => config);
},
};
}

View File

@@ -1,5 +1,4 @@
export * from './adapters';
export type { InsertedLinkType } from './commands';
export * from './embed-linked-doc-block';
export * from './embed-linked-doc-config';
export * from './embed-linked-doc-spec';

View File

@@ -1,5 +1,8 @@
import { Peekable } from '@blocksuite/affine-components/peek';
import { RefNodeSlotsProvider } from '@blocksuite/affine-components/rich-text';
import {
type DocLinkClickedEvent,
RefNodeSlotsProvider,
} from '@blocksuite/affine-components/rich-text';
import {
type AliasInfo,
type DocMode,
@@ -305,11 +308,13 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
.icon(pageId, { params, referenced: true }).value;
});
open = () => {
open = (event?: Partial<DocLinkClickedEvent>) => {
const pageId = this.model.pageId;
if (pageId === this.doc.id) return;
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({ pageId });
this.std
.getOptional(RefNodeSlotsProvider)
?.docLinkClicked.emit({ ...event, pageId });
};
refreshData = () => {

View File

@@ -1,4 +1,7 @@
export { AffineLink, toggleLinkPopup } from './link-node/index.js';
export * from './reference-node/reference-config.js';
export { AffineReference } from './reference-node/reference-node.js';
export type { RefNodeSlots } from './reference-node/types.js';
export type {
DocLinkClickedEvent,
RefNodeSlots,
} from './reference-node/types.js';

View File

@@ -3,6 +3,8 @@ import {
FeatureFlagService,
GenerateDocUrlProvider,
type LinkEventType,
OpenDocExtensionIdentifier,
type OpenDocMode,
type TelemetryEvent,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
@@ -26,11 +28,9 @@ import { join } from 'lit/directives/join.js';
import { repeat } from 'lit/directives/repeat.js';
import {
CenterPeekIcon,
CopyIcon,
DeleteIcon,
EditIcon,
ExpandFullSmallIcon,
MoreVerticalIcon,
OpenIcon,
SmallArrowDownIcon,
@@ -47,6 +47,7 @@ import { RefNodeSlotsProvider } from '../../../../extension/index.js';
import type { AffineInlineEditor } from '../../affine-inline-specs.js';
import { ReferenceAliasPopup } from './reference-alias-popup.js';
import { styles } from './styles.js';
import type { DocLinkClickedEvent } from './types.js';
export class ReferencePopup extends WithDisposable(LitElement) {
static override styles = styles;
@@ -66,10 +67,11 @@ export class ReferencePopup extends WithDisposable(LitElement) {
track(this.std, 'CopiedLink', { control: 'copy link' });
};
private readonly _openDoc = () => {
this.std
.getOptional(RefNodeSlotsProvider)
?.docLinkClicked.emit(this.referenceInfo);
private readonly _openDoc = (event?: Partial<DocLinkClickedEvent>) => {
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
...this.referenceInfo,
...event,
});
};
private readonly _openEditPopup = (e: MouseEvent) => {
@@ -134,8 +136,11 @@ export class ReferencePopup extends WithDisposable(LitElement) {
);
}
get _openButtonDisabled() {
return this.referenceDocId === this.doc.id;
_openButtonDisabled(openMode?: OpenDocMode) {
if (openMode === 'open-in-active-view') {
return this.referenceDocId === this.doc.id;
}
return false;
}
get block() {
@@ -246,28 +251,37 @@ export class ReferencePopup extends WithDisposable(LitElement) {
}
private _openMenuButton() {
const buttons: MenuItem[] = [
{
label: 'Open this doc',
type: 'open-this-doc',
icon: ExpandFullSmallIcon,
action: this._openDoc,
disabled: this._openButtonDisabled,
},
];
const openDocConfig = this.std.get(OpenDocExtensionIdentifier);
// open in new tab
if (isPeekable(this.target)) {
buttons.push({
label: 'Open in center peek',
type: 'open-in-center-peek',
icon: CenterPeekIcon,
action: () => peek(this.target),
});
}
// open in split view
const buttons: MenuItem[] = openDocConfig.items
.map(item => {
if (
(item.type === 'open-in-center-peek' && !isPeekable(this.target)) ||
!openDocConfig?.isAllowed(item.type)
) {
return null;
}
return {
label: item.label,
type: item.type,
icon: item.icon,
action: () => {
if (item.type === 'open-in-center-peek') {
peek(this.target);
} else {
this._openDoc({ openMode: item.type });
}
},
disabled: this._openButtonDisabled(item.type),
when: () => {
if (item.type === 'open-in-center-peek') {
return isPeekable(this.target);
}
return openDocConfig?.isAllowed(item.type) ?? true;
},
};
})
.filter(item => item !== null);
if (buttons.length === 0) {
return nothing;

View File

@@ -1,6 +1,13 @@
import type { ReferenceInfo } from '@blocksuite/affine-model';
import type { OpenDocMode } from '@blocksuite/affine-shared/services';
import type { Slot } from '@blocksuite/global/utils';
export type RefNodeSlots = {
docLinkClicked: Slot<ReferenceInfo>;
export type DocLinkClickedEvent = ReferenceInfo & {
// default is active view
openMode?: OpenDocMode;
event?: MouseEvent;
};
export type RefNodeSlots = {
docLinkClicked: Slot<DocLinkClickedEvent>;
};

View File

@@ -204,6 +204,7 @@ export class EditorMenuAction extends LitElement {
::slotted(svg) {
color: var(--affine-icon-color);
font-size: 20px;
}
`;

View File

@@ -9,6 +9,7 @@ export * from './font-loader';
export * from './generate-url-service';
export * from './native-clipboard-service';
export * from './notification-service';
export * from './open-doc-config';
export * from './page-viewport-service';
export * from './parse-url-service';
export * from './quick-search-service';

View File

@@ -0,0 +1,59 @@
import { createIdentifier } from '@blocksuite/global/di';
import { CenterPeekIcon, ExpandFullIcon } from '@blocksuite/icons/lit';
import { type ExtensionType } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
export type OpenDocMode =
| 'open-in-active-view'
| 'open-in-new-view'
| 'open-in-new-tab'
| 'open-in-center-peek';
// todo: later this will be used to generate the menu items.
// for now we only use it as a hint for whether or not to show the open doc buttons.
export interface OpenDocConfigItem {
type: OpenDocMode;
label: string;
icon: TemplateResult<1>;
}
export interface OpenDocConfig {
items: OpenDocConfigItem[];
}
export interface OpenDocService {
isAllowed: (mode: OpenDocMode) => boolean;
items: OpenDocConfig['items'];
}
export const OpenDocExtensionIdentifier = createIdentifier<OpenDocService>(
'AffineOpenDocExtension'
);
const defaultConfig: OpenDocConfig = {
items: [
{
type: 'open-in-active-view',
label: 'Open this doc',
icon: ExpandFullIcon(),
},
{
type: 'open-in-center-peek',
label: 'Open in center peek',
icon: CenterPeekIcon(),
},
],
};
export const OpenDocExtension = (config: OpenDocConfig): ExtensionType => ({
setup: di => {
di.override(OpenDocExtensionIdentifier, () => {
const allowedOpenDocModes = new Set(config.items.map(item => item.type));
return {
isAllowed: (mode: OpenDocMode) => allowedOpenDocModes.has(mode),
items: config.items,
};
});
},
});
export const DefaultOpenDocExtension = OpenDocExtension(defaultConfig);

View File

@@ -44,6 +44,16 @@ export function isControlledKeyboardEvent(e: KeyboardEvent) {
return e.ctrlKey || e.metaKey || e.altKey;
}
export function isNewTabTrigger(event?: MouseEvent) {
return event
? (event.ctrlKey || event.metaKey || event.button === 1) && !event.altKey
: false;
}
export function isNewViewTrigger(event?: MouseEvent) {
return event ? (event.ctrlKey || event.metaKey) && event.altKey : false;
}
export function on<
T extends HTMLElement,
K extends keyof M,