mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
184
blocksuite/affine/widgets/slash-menu/src/config.ts
Normal file
184
blocksuite/affine/widgets/slash-menu/src/config.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { insertContent } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
ArrowDownBigIcon,
|
||||
ArrowUpBigIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DualLinkIcon,
|
||||
NowIcon,
|
||||
TodayIcon,
|
||||
TomorrowIcon,
|
||||
YesterdayIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { type DeltaInsert, Slice, Text } from '@blocksuite/store';
|
||||
|
||||
import { slashMenuToolTips } from './tooltips';
|
||||
import type { SlashMenuConfig } from './types';
|
||||
import { formatDate, formatTime } from './utils';
|
||||
|
||||
export const defaultSlashMenuConfig: SlashMenuConfig = {
|
||||
items: () => {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Today',
|
||||
icon: TodayIcon(),
|
||||
tooltip: slashMenuToolTips['Today'],
|
||||
description: formatDate(now),
|
||||
group: '6_Date@0',
|
||||
action: ({ std, model }) => {
|
||||
insertContent(std, model, formatDate(now));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Tomorrow',
|
||||
icon: TomorrowIcon(),
|
||||
tooltip: slashMenuToolTips['Tomorrow'],
|
||||
description: formatDate(tomorrow),
|
||||
group: '6_Date@1',
|
||||
action: ({ std, model }) => {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
insertContent(std, model, formatDate(tomorrow));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Yesterday',
|
||||
icon: YesterdayIcon(),
|
||||
tooltip: slashMenuToolTips['Yesterday'],
|
||||
description: formatDate(yesterday),
|
||||
group: '6_Date@2',
|
||||
action: ({ std, model }) => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
insertContent(std, model, formatDate(yesterday));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Now',
|
||||
icon: NowIcon(),
|
||||
tooltip: slashMenuToolTips['Now'],
|
||||
description: formatTime(now),
|
||||
group: '6_Date@3',
|
||||
action: ({ std, model }) => {
|
||||
insertContent(std, model, formatTime(now));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Move Up',
|
||||
description: 'Shift this line up.',
|
||||
icon: ArrowUpBigIcon(),
|
||||
tooltip: slashMenuToolTips['Move Up'],
|
||||
group: '8_Actions@0',
|
||||
action: ({ std, model }) => {
|
||||
const { host } = std;
|
||||
const previousSiblingModel = host.doc.getPrev(model);
|
||||
if (!previousSiblingModel) return;
|
||||
|
||||
const parentModel = host.doc.getParent(previousSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
host.doc.moveBlocks([model], parentModel, previousSiblingModel, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Move Down',
|
||||
description: 'Shift this line down.',
|
||||
icon: ArrowDownBigIcon(),
|
||||
tooltip: slashMenuToolTips['Move Down'],
|
||||
group: '8_Actions@1',
|
||||
action: ({ std, model }) => {
|
||||
const { host } = std;
|
||||
const nextSiblingModel = host.doc.getNext(model);
|
||||
if (!nextSiblingModel) return;
|
||||
|
||||
const parentModel = host.doc.getParent(nextSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
host.doc.moveBlocks([model], parentModel, nextSiblingModel, false);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
description: 'Copy this line to clipboard.',
|
||||
icon: CopyIcon(),
|
||||
tooltip: slashMenuToolTips['Copy'],
|
||||
group: '8_Actions@2',
|
||||
action: ({ std, model }) => {
|
||||
const slice = Slice.fromModels(std.store, [model]);
|
||||
|
||||
std.clipboard
|
||||
.copy(slice)
|
||||
.then(() => {
|
||||
toast(std.host, 'Copied to clipboard');
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Duplicate',
|
||||
description: 'Create a duplicate of this line.',
|
||||
icon: DualLinkIcon(),
|
||||
tooltip: slashMenuToolTips['Copy'],
|
||||
group: '8_Actions@3',
|
||||
action: ({ std, model }) => {
|
||||
if (!model.text || !(model.text instanceof Text)) {
|
||||
console.error("Can't duplicate a block without text");
|
||||
return;
|
||||
}
|
||||
const { host } = std;
|
||||
const parent = host.doc.getParent(model);
|
||||
if (!parent) {
|
||||
console.error(
|
||||
'Failed to duplicate block! Parent not found: ' +
|
||||
model.id +
|
||||
'|' +
|
||||
model.flavour
|
||||
);
|
||||
return;
|
||||
}
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
// FIXME: this clone is not correct
|
||||
host.doc.addBlock(
|
||||
model.flavour,
|
||||
{
|
||||
type: (model as ParagraphBlockModel).props.type,
|
||||
text: new Text(
|
||||
(
|
||||
model as ParagraphBlockModel
|
||||
).props.text.toDelta() as DeltaInsert[]
|
||||
),
|
||||
checked: (model as ListBlockModel).props.checked,
|
||||
},
|
||||
host.doc.getParent(model),
|
||||
index
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
description: 'Remove this line permanently.',
|
||||
searchAlias: ['remove'],
|
||||
icon: DeleteIcon(),
|
||||
tooltip: slashMenuToolTips['Delete'],
|
||||
group: '8_Actions@4',
|
||||
action: ({ std, model }) => {
|
||||
std.host.doc.deleteBlock(model);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
4
blocksuite/affine/widgets/slash-menu/src/consts.ts
Normal file
4
blocksuite/affine/widgets/slash-menu/src/consts.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const AFFINE_SLASH_MENU_WIDGET = 'affine-slash-menu-widget';
|
||||
export const AFFINE_SLASH_MENU_TRIGGER_KEY = '/';
|
||||
export const AFFINE_SLASH_MENU_TOOLTIP_TIMEOUT = 800;
|
||||
export const AFFINE_SLASH_MENU_MAX_HEIGHT = 390;
|
||||
15
blocksuite/affine/widgets/slash-menu/src/effects.ts
Normal file
15
blocksuite/affine/widgets/slash-menu/src/effects.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { AFFINE_SLASH_MENU_WIDGET } from './consts';
|
||||
import { InnerSlashMenu, SlashMenu } from './slash-menu-popover';
|
||||
import { AffineSlashMenuWidget } from './widget';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_SLASH_MENU_WIDGET, AffineSlashMenuWidget);
|
||||
customElements.define('affine-slash-menu', SlashMenu);
|
||||
customElements.define('inner-slash-menu', InnerSlashMenu);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_SLASH_MENU_WIDGET]: AffineSlashMenuWidget;
|
||||
}
|
||||
}
|
||||
51
blocksuite/affine/widgets/slash-menu/src/extensions.ts
Normal file
51
blocksuite/affine/widgets/slash-menu/src/extensions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
StdIdentifier,
|
||||
WidgetViewExtension,
|
||||
} from '@blocksuite/std';
|
||||
import { Extension, type ExtensionType } from '@blocksuite/store';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { defaultSlashMenuConfig } from './config';
|
||||
import { AFFINE_SLASH_MENU_WIDGET } from './consts';
|
||||
import type { SlashMenuConfig } from './types';
|
||||
import { mergeSlashMenuConfigs } from './utils';
|
||||
|
||||
export class SlashMenuExtension extends Extension {
|
||||
config: SlashMenuConfig;
|
||||
|
||||
static override setup(di: Container) {
|
||||
WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_SLASH_MENU_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_SLASH_MENU_WIDGET)}`
|
||||
).setup(di);
|
||||
|
||||
di.add(this, [StdIdentifier]);
|
||||
|
||||
SlashMenuConfigExtension('default', defaultSlashMenuConfig).setup(di);
|
||||
}
|
||||
|
||||
constructor(readonly std: BlockStdScope) {
|
||||
super();
|
||||
this.config = mergeSlashMenuConfigs(
|
||||
this.std.provider.getAll(SlashMenuConfigIdentifier)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const SlashMenuConfigIdentifier = createIdentifier<SlashMenuConfig>(
|
||||
`${AFFINE_SLASH_MENU_WIDGET}-config`
|
||||
);
|
||||
|
||||
export function SlashMenuConfigExtension(
|
||||
id: string,
|
||||
config: SlashMenuConfig
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(SlashMenuConfigIdentifier(id), config);
|
||||
},
|
||||
};
|
||||
}
|
||||
3
blocksuite/affine/widgets/slash-menu/src/index.ts
Normal file
3
blocksuite/affine/widgets/slash-menu/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { AFFINE_SLASH_MENU_WIDGET } from './consts';
|
||||
export * from './extensions';
|
||||
export * from './types';
|
||||
668
blocksuite/affine/widgets/slash-menu/src/slash-menu-popover.ts
Normal file
668
blocksuite/affine/widgets/slash-menu/src/slash-menu-popover.ts
Normal file
@@ -0,0 +1,668 @@
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import {
|
||||
cleanSpecifiedTail,
|
||||
getInlineEditorByModel,
|
||||
getTextContentFromInlineRange,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
createKeydownObserver,
|
||||
getCurrentNativeRange,
|
||||
getPopperPosition,
|
||||
isControlledKeyboardEvent,
|
||||
isFuzzyMatch,
|
||||
substringMatchScore,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { autoPlacement, offset } from '@floating-ui/dom';
|
||||
import { html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
|
||||
import {
|
||||
AFFINE_SLASH_MENU_MAX_HEIGHT,
|
||||
AFFINE_SLASH_MENU_TOOLTIP_TIMEOUT,
|
||||
AFFINE_SLASH_MENU_TRIGGER_KEY,
|
||||
} from './consts.js';
|
||||
import { slashItemToolTipStyle, styles } from './styles.js';
|
||||
import type {
|
||||
SlashMenuActionItem,
|
||||
SlashMenuContext,
|
||||
SlashMenuItem,
|
||||
SlashMenuSubMenu,
|
||||
} from './types.js';
|
||||
import {
|
||||
isActionItem,
|
||||
isSubMenuItem,
|
||||
parseGroup,
|
||||
slashItemClassName,
|
||||
} from './utils.js';
|
||||
type InnerSlashMenuContext = SlashMenuContext & {
|
||||
onClickItem: (item: SlashMenuActionItem) => void;
|
||||
searching: boolean;
|
||||
};
|
||||
|
||||
export class SlashMenu extends WithDisposable(LitElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private get _telemetry() {
|
||||
return this.context.std.getOptional(TelemetryProvider);
|
||||
}
|
||||
|
||||
private get _editorMode() {
|
||||
return this.context.std.get(DocModeProvider).getEditorMode();
|
||||
}
|
||||
|
||||
private readonly _handleClickItem = (item: SlashMenuActionItem) => {
|
||||
// Need to remove the search string
|
||||
// We must to do clean the slash string before we do the action
|
||||
// Otherwise, the action may change the model and cause the slash string to be changed
|
||||
cleanSpecifiedTail(
|
||||
this.context.std,
|
||||
this.context.model,
|
||||
AFFINE_SLASH_MENU_TRIGGER_KEY + (this._query || '')
|
||||
);
|
||||
this.inlineEditor
|
||||
.waitForUpdate()
|
||||
.then(() => {
|
||||
item.action(this.context);
|
||||
this._telemetry?.track('SelectSlashMenuItem', {
|
||||
page: this._editorMode ?? undefined,
|
||||
segment:
|
||||
this.context.model.flavour === 'affine:edgeless-text'
|
||||
? 'edgeless-text'
|
||||
: 'doc',
|
||||
module: 'slash menu',
|
||||
control: item.name,
|
||||
});
|
||||
this.abortController.abort();
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
private readonly _initItemPathMap = () => {
|
||||
const traverse = (item: SlashMenuItem, path: number[]) => {
|
||||
this._itemPathMap.set(item, [...path]);
|
||||
if (isSubMenuItem(item)) {
|
||||
item.subMenu.forEach((subItem, index) =>
|
||||
traverse(subItem, [...path, index])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.items.forEach((item, index) => traverse(item, [index]));
|
||||
};
|
||||
|
||||
private _innerSlashMenuContext!: InnerSlashMenuContext;
|
||||
|
||||
private readonly _itemPathMap = new Map<SlashMenuItem, number[]>();
|
||||
|
||||
private _queryState: 'off' | 'on' | 'no_result' = 'off';
|
||||
|
||||
private readonly _startRange = this.inlineEditor.getInlineRange();
|
||||
|
||||
private readonly _updateFilteredItems = () => {
|
||||
const query = this._query;
|
||||
if (query === null) {
|
||||
this.abortController.abort();
|
||||
return;
|
||||
}
|
||||
this._filteredItems = [];
|
||||
const searchStr = query.toLowerCase();
|
||||
if (searchStr === '' || searchStr.endsWith(' ')) {
|
||||
this._queryState = searchStr === '' ? 'off' : 'no_result';
|
||||
this._innerSlashMenuContext.searching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Layer order traversal
|
||||
let depth = 0;
|
||||
let queue = this.items;
|
||||
while (queue.length !== 0) {
|
||||
// remove the sub menu item from the previous layer result
|
||||
this._filteredItems = this._filteredItems.filter(
|
||||
item => !isSubMenuItem(item)
|
||||
);
|
||||
|
||||
this._filteredItems = this._filteredItems.concat(
|
||||
queue.filter(({ name, searchAlias = [] }) =>
|
||||
[name, ...searchAlias].some(str => isFuzzyMatch(str, searchStr))
|
||||
)
|
||||
);
|
||||
|
||||
// We search first and second layer
|
||||
if (this._filteredItems.length !== 0 && depth >= 1) break;
|
||||
|
||||
queue = queue
|
||||
.map<typeof queue>(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return item.subMenu;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
this._filteredItems.sort((a, b) => {
|
||||
return -(
|
||||
substringMatchScore(a.name, searchStr) -
|
||||
substringMatchScore(b.name, searchStr)
|
||||
);
|
||||
});
|
||||
|
||||
this._queryState = this._filteredItems.length === 0 ? 'no_result' : 'on';
|
||||
this._innerSlashMenuContext.searching = true;
|
||||
};
|
||||
|
||||
private get _query() {
|
||||
return getTextContentFromInlineRange(this.inlineEditor, this._startRange);
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.context.std.host;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly inlineEditor: AffineInlineEditor,
|
||||
private readonly abortController = new AbortController()
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._innerSlashMenuContext = {
|
||||
...this.context,
|
||||
onClickItem: this._handleClickItem,
|
||||
searching: false,
|
||||
};
|
||||
|
||||
this._initItemPathMap();
|
||||
|
||||
this._disposables.addFromEvent(this, 'mousedown', e => {
|
||||
// Prevent input from losing focus
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (!inlineEditor || !inlineEditor.eventSource) {
|
||||
console.error('inlineEditor or eventSource is not found');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle arrow key
|
||||
*
|
||||
* The slash menu will be closed in the following keyboard cases:
|
||||
* - Press the space key
|
||||
* - Press the backspace key and the search string is empty
|
||||
* - Press the escape key
|
||||
* - When the search item is empty, the slash menu will be hidden temporarily,
|
||||
* and if the following key is not the backspace key, the slash menu will be closed
|
||||
*/
|
||||
createKeydownObserver({
|
||||
target: inlineEditor.eventSource,
|
||||
signal: this.abortController.signal,
|
||||
interceptor: (event, next) => {
|
||||
const { key, isComposing, code } = event;
|
||||
if (key === AFFINE_SLASH_MENU_TRIGGER_KEY) {
|
||||
// Can not stopPropagation here,
|
||||
// otherwise the rich text will not be able to trigger a new the slash menu
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'Process' && !isComposing && code === 'Slash') {
|
||||
// The IME case of above
|
||||
return;
|
||||
}
|
||||
|
||||
if (key !== 'Backspace' && this._queryState === 'no_result') {
|
||||
// if the following key is not the backspace key,
|
||||
// the slash menu will be closed
|
||||
this.abortController.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'Escape') {
|
||||
this.abortController.abort();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'ArrowRight' || key === 'ArrowLeft') {
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
onInput: isComposition => {
|
||||
if (isComposition) {
|
||||
this._updateFilteredItems();
|
||||
} else {
|
||||
const subscription = this.inlineEditor.slots.renderComplete.subscribe(
|
||||
() => {
|
||||
subscription.unsubscribe();
|
||||
this._updateFilteredItems();
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
onPaste: () => {
|
||||
setTimeout(() => {
|
||||
this._updateFilteredItems();
|
||||
}, 50);
|
||||
},
|
||||
onDelete: () => {
|
||||
const curRange = this.inlineEditor.getInlineRange();
|
||||
if (!this._startRange || !curRange) {
|
||||
return;
|
||||
}
|
||||
if (curRange.index < this._startRange.index) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
const subscription = this.inlineEditor.slots.renderComplete.subscribe(
|
||||
() => {
|
||||
subscription.unsubscribe();
|
||||
this._updateFilteredItems();
|
||||
}
|
||||
);
|
||||
},
|
||||
onAbort: () => this.abortController.abort(),
|
||||
});
|
||||
|
||||
this._telemetry?.track('OpenSlashMenu', {
|
||||
page: this._editorMode ?? undefined,
|
||||
type: this.context.model.flavour.split(':').pop(),
|
||||
module: 'slash menu',
|
||||
});
|
||||
}
|
||||
|
||||
protected override willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
const currRage = getCurrentNativeRange();
|
||||
if (!currRage) {
|
||||
this.abortController.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle position
|
||||
const updatePosition = throttle(() => {
|
||||
this._position = getPopperPosition(this, currRage);
|
||||
}, 10);
|
||||
|
||||
this.disposables.addFromEvent(window, 'resize', updatePosition);
|
||||
updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const slashMenuStyles = this._position
|
||||
? {
|
||||
transform: `translate(${this._position.x}, ${this._position.y})`,
|
||||
maxHeight: `${Math.min(this._position.height, AFFINE_SLASH_MENU_MAX_HEIGHT)}px`,
|
||||
}
|
||||
: {
|
||||
visibility: 'hidden',
|
||||
};
|
||||
|
||||
return html`${this._queryState !== 'no_result'
|
||||
? html` <div
|
||||
class="overlay-mask"
|
||||
@click="${() => this.abortController.abort()}"
|
||||
></div>`
|
||||
: nothing}
|
||||
<inner-slash-menu
|
||||
.context=${this._innerSlashMenuContext}
|
||||
.menu=${this._queryState === 'off' ? this.items : this._filteredItems}
|
||||
.mainMenuStyle=${slashMenuStyles}
|
||||
.abortController=${this.abortController}
|
||||
>
|
||||
</inner-slash-menu>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _filteredItems: (SlashMenuActionItem | SlashMenuSubMenu)[] =
|
||||
[];
|
||||
|
||||
@state()
|
||||
private accessor _position: {
|
||||
x: string;
|
||||
y: string;
|
||||
height: number;
|
||||
} | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor items!: SlashMenuItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor context!: SlashMenuContext;
|
||||
}
|
||||
|
||||
export class InnerSlashMenu extends WithDisposable(LitElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _closeSubMenu = () => {
|
||||
this._subMenuAbortController?.abort();
|
||||
this._subMenuAbortController = null;
|
||||
this._currentSubMenu = null;
|
||||
};
|
||||
|
||||
private _currentSubMenu: SlashMenuSubMenu | null = null;
|
||||
|
||||
private readonly _openSubMenu = (item: SlashMenuSubMenu) => {
|
||||
if (item === this._currentSubMenu) return;
|
||||
|
||||
const itemElement = this.shadowRoot?.querySelector(
|
||||
`.${slashItemClassName(item)}`
|
||||
);
|
||||
if (!itemElement) return;
|
||||
|
||||
this._closeSubMenu();
|
||||
this._currentSubMenu = item;
|
||||
this._subMenuAbortController = new AbortController();
|
||||
this._subMenuAbortController.signal.addEventListener('abort', () => {
|
||||
this._closeSubMenu();
|
||||
});
|
||||
|
||||
const subMenuElement = createLitPortal({
|
||||
shadowDom: false,
|
||||
template: html`<inner-slash-menu
|
||||
.context=${this.context}
|
||||
.menu=${item.subMenu}
|
||||
.depth=${this.depth + 1}
|
||||
.abortController=${this._subMenuAbortController}
|
||||
>
|
||||
${item.subMenu.map(this._renderItem)}
|
||||
</inner-slash-menu>`,
|
||||
computePosition: {
|
||||
referenceElement: itemElement,
|
||||
autoUpdate: true,
|
||||
middleware: [
|
||||
offset(12),
|
||||
autoPlacement({
|
||||
allowedPlacements: ['right-start', 'right-end'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
abortController: this._subMenuAbortController,
|
||||
});
|
||||
|
||||
subMenuElement.style.zIndex = `calc(var(--affine-z-index-popover) + ${this.depth})`;
|
||||
subMenuElement.focus();
|
||||
};
|
||||
|
||||
private readonly _renderActionItem = (item: SlashMenuActionItem) => {
|
||||
const { name, icon, description, tooltip } = item;
|
||||
|
||||
const hover = item === this._activeItem;
|
||||
|
||||
return html`<icon-button
|
||||
class="slash-menu-item ${slashItemClassName(item)}"
|
||||
width="100%"
|
||||
height="44px"
|
||||
text=${name}
|
||||
subText=${ifDefined(description)}
|
||||
data-testid="${name}"
|
||||
hover=${hover}
|
||||
@mousemove=${() => {
|
||||
this._activeItem = item;
|
||||
this._closeSubMenu();
|
||||
}}
|
||||
@click=${() => this.context.onClickItem(item)}
|
||||
>
|
||||
${icon && html`<div class="slash-menu-item-icon">${icon}</div>`}
|
||||
${tooltip &&
|
||||
html`<affine-tooltip
|
||||
tip-position="right"
|
||||
.offset=${22}
|
||||
.tooltipStyle=${slashItemToolTipStyle}
|
||||
.hoverOptions=${{
|
||||
enterDelay: AFFINE_SLASH_MENU_TOOLTIP_TIMEOUT,
|
||||
allowMultiple: false,
|
||||
}}
|
||||
>
|
||||
<div class="tooltip-figure">${tooltip.figure}</div>
|
||||
<div class="tooltip-caption">${tooltip.caption}</div>
|
||||
</affine-tooltip>`}
|
||||
</icon-button>`;
|
||||
};
|
||||
|
||||
private readonly _renderGroup = (
|
||||
groupName: string,
|
||||
items: SlashMenuItem[]
|
||||
) => {
|
||||
return html`<div class="slash-menu-group">
|
||||
${when(
|
||||
!this.context.searching,
|
||||
() => html`<div class="slash-menu-group-name">${groupName}</div>`
|
||||
)}
|
||||
${items.map(this._renderItem)}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
private readonly _renderItem = (item: SlashMenuItem) => {
|
||||
if (isActionItem(item)) return this._renderActionItem(item);
|
||||
if (isSubMenuItem(item)) return this._renderSubMenuItem(item);
|
||||
return nothing;
|
||||
};
|
||||
|
||||
private readonly _renderSubMenuItem = (item: SlashMenuSubMenu) => {
|
||||
const { name, icon, description } = item;
|
||||
|
||||
const hover = item === this._activeItem;
|
||||
|
||||
return html`<icon-button
|
||||
class="slash-menu-item ${slashItemClassName(item)}"
|
||||
width="100%"
|
||||
height="44px"
|
||||
text=${name}
|
||||
subText=${ifDefined(description)}
|
||||
data-testid="${name}"
|
||||
hover=${hover}
|
||||
@mousemove=${() => {
|
||||
this._activeItem = item;
|
||||
this._openSubMenu(item);
|
||||
}}
|
||||
@touchstart=${() => {
|
||||
isSubMenuItem(item) &&
|
||||
(this._currentSubMenu === item
|
||||
? this._closeSubMenu()
|
||||
: this._openSubMenu(item));
|
||||
}}
|
||||
>
|
||||
${icon && html`<div class="slash-menu-item-icon">${icon}</div>`}
|
||||
<div slot="suffix" style="transform: rotate(-90deg);">
|
||||
${ArrowDownSmallIcon()}
|
||||
</div>
|
||||
</icon-button>`;
|
||||
};
|
||||
|
||||
private _subMenuAbortController: AbortController | null = null;
|
||||
|
||||
private _scrollToItem(item: SlashMenuItem) {
|
||||
const shadowRoot = this.shadowRoot;
|
||||
if (!shadowRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ele = shadowRoot.querySelector(`icon-button[text="${item.name}"]`);
|
||||
if (!ele) {
|
||||
return;
|
||||
}
|
||||
ele.scrollIntoView({
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// close all sub menus
|
||||
this.abortController?.signal?.addEventListener('abort', () => {
|
||||
this._subMenuAbortController?.abort();
|
||||
});
|
||||
this.addEventListener('wheel', event => {
|
||||
if (this._currentSubMenu) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
this.context.std,
|
||||
this.context.model
|
||||
);
|
||||
|
||||
if (!inlineEditor || !inlineEditor.eventSource) {
|
||||
console.error('inlineEditor or eventSource is not found');
|
||||
return;
|
||||
}
|
||||
|
||||
inlineEditor.eventSource.addEventListener(
|
||||
'keydown',
|
||||
event => {
|
||||
if (this._currentSubMenu) return;
|
||||
if (event.isComposing) return;
|
||||
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = event;
|
||||
|
||||
const onlyCmd = (ctrlKey || metaKey) && !altKey && !shiftKey;
|
||||
const onlyShift = shiftKey && !isControlledKeyboardEvent(event);
|
||||
const notControlShift = !(ctrlKey || metaKey || altKey || shiftKey);
|
||||
|
||||
let moveStep = 0;
|
||||
if (
|
||||
(key === 'ArrowUp' && notControlShift) ||
|
||||
(key === 'Tab' && onlyShift) ||
|
||||
(key === 'P' && onlyCmd) ||
|
||||
(key === 'p' && onlyCmd)
|
||||
) {
|
||||
moveStep = -1;
|
||||
}
|
||||
|
||||
if (
|
||||
(key === 'ArrowDown' && notControlShift) ||
|
||||
(key === 'Tab' && notControlShift) ||
|
||||
(key === 'n' && onlyCmd) ||
|
||||
(key === 'N' && onlyCmd)
|
||||
) {
|
||||
moveStep = 1;
|
||||
}
|
||||
|
||||
if (moveStep !== 0) {
|
||||
const activeItemIndex = this.menu.indexOf(this._activeItem);
|
||||
const itemIndex =
|
||||
(activeItemIndex + moveStep + this.menu.length) % this.menu.length;
|
||||
|
||||
this._activeItem = this.menu[itemIndex] as typeof this._activeItem;
|
||||
this._scrollToItem(this._activeItem);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (key === 'ArrowRight' && notControlShift) {
|
||||
if (isSubMenuItem(this._activeItem)) {
|
||||
this._openSubMenu(this._activeItem);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (key === 'ArrowLeft' && notControlShift) {
|
||||
if (this.depth != 0) this.abortController.abort();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (key === 'Escape' && notControlShift) {
|
||||
this.abortController.abort();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (key === 'Enter' && notControlShift) {
|
||||
if (isSubMenuItem(this._activeItem)) {
|
||||
this._openSubMenu(this._activeItem);
|
||||
} else if (isActionItem(this._activeItem)) {
|
||||
this.context.onClickItem(this._activeItem);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
{
|
||||
capture: true,
|
||||
signal: this.abortController.signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
this.abortController.abort();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.menu.length === 0) return nothing;
|
||||
|
||||
const style = styleMap(this.mainMenuStyle ?? { position: 'relative' });
|
||||
|
||||
const groups = groupBy(this.menu, ({ group }) =>
|
||||
group && !this.context.searching ? parseGroup(group)[1] : ''
|
||||
);
|
||||
|
||||
return html`<div
|
||||
class="slash-menu"
|
||||
style=${style}
|
||||
data-testid=${`sub-menu-${this.depth}`}
|
||||
>
|
||||
${Object.entries(groups).map(([groupName, items]) =>
|
||||
this._renderGroup(groupName, items)
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('menu') && this.menu.length !== 0) {
|
||||
this._activeItem = this.menu[0];
|
||||
|
||||
// this case happen on query updated
|
||||
this._subMenuAbortController?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _activeItem!: SlashMenuActionItem | SlashMenuSubMenu;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor abortController!: AbortController;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor context!: InnerSlashMenuContext;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor depth: number = 0;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor mainMenuStyle: Parameters<typeof styleMap>[0] | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor menu!: SlashMenuItem[];
|
||||
}
|
||||
109
blocksuite/affine/widgets/slash-menu/src/styles.ts
Normal file
109
blocksuite/affine/widgets/slash-menu/src/styles.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
.overlay-mask {
|
||||
pointer-events: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 4px 8px 8px;
|
||||
width: 280px;
|
||||
overflow-y: auto;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
|
||||
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
border-radius: 8px;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
user-select: none;
|
||||
/* transition: max-height 0.2s ease-in-out; */
|
||||
}
|
||||
|
||||
${scrollbarStyle('.slash-menu')}
|
||||
|
||||
.slash-menu-group-name {
|
||||
box-sizing: border-box;
|
||||
padding: 2px 8px;
|
||||
|
||||
font-size: var(--affine-font-xs);
|
||||
font-weight: 500;
|
||||
line-height: var(--affine-line-height);
|
||||
text-align: left;
|
||||
color: var(
|
||||
--light-textColor-textSecondaryColor,
|
||||
var(--textColor-textSecondaryColor, #8e8d91)
|
||||
);
|
||||
}
|
||||
|
||||
.slash-menu-item {
|
||||
padding: 2px 8px 2px 8px;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slash-menu-item-icon {
|
||||
box-sizing: border-box;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
border-radius: 4px;
|
||||
color: var(--affine-icon-color);
|
||||
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slash-menu-item-icon svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slash-menu-item.ask-ai {
|
||||
color: var(--affine-brand-color);
|
||||
}
|
||||
.slash-menu-item.github .github-icon {
|
||||
color: var(--affine-black);
|
||||
}
|
||||
`;
|
||||
|
||||
export const slashItemToolTipStyle = css`
|
||||
.affine-tooltip {
|
||||
display: flex;
|
||||
padding: 4px 4px 2px 4px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tooltip-figure svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tooltip-caption {
|
||||
padding-left: 4px;
|
||||
color: var(
|
||||
--light-textColor-textSecondaryColor,
|
||||
var(--textColor-textSecondaryColor, #8e8d91)
|
||||
);
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
line-height: var(--affine-line-height);
|
||||
}
|
||||
`;
|
||||
13
blocksuite/affine/widgets/slash-menu/src/tooltips/copy.ts
Normal file
13
blocksuite/affine/widgets/slash-menu/src/tooltips/copy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const CopyTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1240" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1240)">
|
||||
<path d="M4 7C4 5.89543 4.89543 5 6 5H172V32H6C4.89543 32 4 31.1046 4 30V7Z" fill="#F4F4F5"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
14
blocksuite/affine/widgets/slash-menu/src/tooltips/delete.ts
Normal file
14
blocksuite/affine/widgets/slash-menu/src/tooltips/delete.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const DeleteTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1246" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1246)">
|
||||
<path d="M4 7C4 5.89543 4.89543 5 6 5H172V32H6C4.89543 32 4 31.1046 4 30V7Z" fill="#FDECEB"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#EB4335" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
11
blocksuite/affine/widgets/slash-menu/src/tooltips/empty.ts
Normal file
11
blocksuite/affine/widgets/slash-menu/src/tooltips/empty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const EmptyTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_864" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_864)">
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
51
blocksuite/affine/widgets/slash-menu/src/tooltips/index.ts
Normal file
51
blocksuite/affine/widgets/slash-menu/src/tooltips/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { SlashMenuTooltip } from '../types';
|
||||
import { CopyTooltip } from './copy';
|
||||
import { DeleteTooltip } from './delete';
|
||||
import { MoveDownTooltip } from './move-down';
|
||||
import { MoveUpTooltip } from './move-up';
|
||||
import { NowTooltip } from './now';
|
||||
import { TodayTooltip } from './today';
|
||||
import { TomorrowTooltip } from './tomorrow';
|
||||
import { YesterdayTooltip } from './yesterday';
|
||||
|
||||
export const slashMenuToolTips: Record<string, SlashMenuTooltip> = {
|
||||
Today: {
|
||||
figure: TodayTooltip,
|
||||
caption: 'Today',
|
||||
},
|
||||
|
||||
Tomorrow: {
|
||||
figure: TomorrowTooltip,
|
||||
caption: 'Tomorrow',
|
||||
},
|
||||
|
||||
Yesterday: {
|
||||
figure: YesterdayTooltip,
|
||||
caption: 'Yesterday',
|
||||
},
|
||||
|
||||
Now: {
|
||||
figure: NowTooltip,
|
||||
caption: 'Now',
|
||||
},
|
||||
|
||||
'Move Up': {
|
||||
figure: MoveUpTooltip,
|
||||
caption: 'Move Up',
|
||||
},
|
||||
|
||||
'Move Down': {
|
||||
figure: MoveDownTooltip,
|
||||
caption: 'Move Down',
|
||||
},
|
||||
|
||||
Copy: {
|
||||
figure: CopyTooltip,
|
||||
caption: 'Copy / Duplicate',
|
||||
},
|
||||
|
||||
Delete: {
|
||||
figure: DeleteTooltip,
|
||||
caption: 'Delete',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const MoveDownTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1234" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1234)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
<text fill="#A9A9AD" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
<g clip-path="url(#clip0_16460_1234)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.5073 51.0032C25.2022 50.723 24.7278 50.7432 24.4476 51.0483L20.75 55.0745L20.75 43C20.75 42.5858 20.4142 42.25 20 42.25C19.5858 42.25 19.25 42.5858 19.25 43L19.25 55.0745L15.5524 51.0483C15.2722 50.7432 14.7978 50.723 14.4927 51.0032C14.1876 51.2833 14.1674 51.7578 14.4476 52.0629L19.4476 57.5073C19.5896 57.662 19.79 57.75 20 57.75C20.21 57.75 20.4104 57.662 20.5524 57.5073L25.5524 52.0629C25.8326 51.7578 25.8124 51.2833 25.5073 51.0032Z" fill="#121212"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16460_1234">
|
||||
<rect width="24" height="24" fill="white" transform="translate(8 38)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
21
blocksuite/affine/widgets/slash-menu/src/tooltips/move-up.ts
Normal file
21
blocksuite/affine/widgets/slash-menu/src/tooltips/move-up.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const MoveUpTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1228" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1228)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#A9A9AD" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
<g clip-path="url(#clip0_16460_1228)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.5073 16.9968C25.2022 17.277 24.7278 17.2568 24.4476 16.9517L20.75 12.9255L20.75 25C20.75 25.4142 20.4142 25.75 20 25.75C19.5858 25.75 19.25 25.4142 19.25 25L19.25 12.9255L15.5524 16.9517C15.2722 17.2568 14.7978 17.277 14.4927 16.9968C14.1876 16.7167 14.1674 16.2422 14.4476 15.9371L19.4476 10.4927C19.5896 10.338 19.79 10.25 20 10.25C20.21 10.25 20.4104 10.338 20.5524 10.4927L25.5524 15.9371C25.8326 16.2422 25.8124 16.7167 25.5073 16.9968Z" fill="#121212"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16460_1228">
|
||||
<rect width="24" height="24" fill="white" transform="translate(8 6)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
14
blocksuite/affine/widgets/slash-menu/src/tooltips/now.ts
Normal file
14
blocksuite/affine/widgets/slash-menu/src/tooltips/now.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const NowTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1143" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1143)">
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="38.0488" y="16.6364">now</tspan><tspan x="81.8574" y="16.6364"> and time. </tspan></text>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Insert </tspan><tspan x="57.8047" y="16.6364"> date</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="34.6364">11:45:14 Wed 3 Aug, 2022</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
14
blocksuite/affine/widgets/slash-menu/src/tooltips/today.ts
Normal file
14
blocksuite/affine/widgets/slash-menu/src/tooltips/today.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const TodayTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1128" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1128)">
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="95.5098" y="16.6364">. </tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="34.6364">Wed 3 Aug, 2022</tspan></text>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Insert today’s date</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const TomorrowTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1133" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1133)">
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="38.0488" y="16.6364">tomorrow’s</tspan><tspan x="114.211" y="16.6364">. </tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="34.6364">Wed 3 Aug, 2022</tspan></text>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Insert </tspan><tspan x="90.1582" y="16.6364"> date</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const YesterdayTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1138" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1138)">
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="38.0488" y="16.6364">yesterday’s</tspan><tspan x="115.334" y="16.6364">. </tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="34.6364">Wed 3 Aug, 2022</tspan></text>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Insert </tspan><tspan x="91.2812" y="16.6364"> date</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
59
blocksuite/affine/widgets/slash-menu/src/types.ts
Normal file
59
blocksuite/affine/widgets/slash-menu/src/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export type SlashMenuContext = {
|
||||
std: BlockStdScope;
|
||||
model: BlockModel;
|
||||
};
|
||||
|
||||
export type SlashMenuTooltip = {
|
||||
figure: TemplateResult;
|
||||
caption: string;
|
||||
};
|
||||
|
||||
type SlashMenuItemBase = {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: TemplateResult;
|
||||
/**
|
||||
* This field defines sorting and grouping of menu items like VSCode.
|
||||
* The first number indicates the group index, the second number indicates the item index in the group.
|
||||
* The group name is the string between `_` and `@`.
|
||||
* You can find an example figure in https://code.visualstudio.com/api/references/contribution-points#menu-example
|
||||
*/
|
||||
group?: `${number}_${string}@${number}`;
|
||||
|
||||
searchAlias?: string[];
|
||||
/**
|
||||
* The condition to show the menu item.
|
||||
*/
|
||||
when?: (ctx: SlashMenuContext) => boolean;
|
||||
};
|
||||
|
||||
export type SlashMenuActionItem = SlashMenuItemBase & {
|
||||
action: (ctx: SlashMenuContext) => void;
|
||||
tooltip?: SlashMenuTooltip;
|
||||
/**
|
||||
* The alias of the menu item for search.
|
||||
*/
|
||||
searchAlias?: string[];
|
||||
};
|
||||
|
||||
export type SlashMenuSubMenu = SlashMenuItemBase & {
|
||||
subMenu: SlashMenuItem[];
|
||||
};
|
||||
|
||||
export type SlashMenuItem = SlashMenuActionItem | SlashMenuSubMenu;
|
||||
|
||||
export type SlashMenuConfig = {
|
||||
/**
|
||||
* The items in the slash menu. It can be generated dynamically with the context.
|
||||
*/
|
||||
items: SlashMenuItem[] | ((ctx: SlashMenuContext) => SlashMenuItem[]);
|
||||
|
||||
/**
|
||||
* Slash menu will not be triggered when the condition is true.
|
||||
*/
|
||||
disableWhen?: (ctx: SlashMenuContext) => boolean;
|
||||
};
|
||||
105
blocksuite/affine/widgets/slash-menu/src/utils.ts
Normal file
105
blocksuite/affine/widgets/slash-menu/src/utils.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type {
|
||||
SlashMenuActionItem,
|
||||
SlashMenuConfig,
|
||||
SlashMenuContext,
|
||||
SlashMenuItem,
|
||||
SlashMenuSubMenu,
|
||||
} from './types';
|
||||
|
||||
export function isActionItem(item: SlashMenuItem): item is SlashMenuActionItem {
|
||||
return 'action' in item;
|
||||
}
|
||||
|
||||
export function isSubMenuItem(item: SlashMenuItem): item is SlashMenuSubMenu {
|
||||
return 'subMenu' in item;
|
||||
}
|
||||
|
||||
export function slashItemClassName({ name }: SlashMenuItem) {
|
||||
return name.split(' ').join('-').toLocaleLowerCase();
|
||||
}
|
||||
|
||||
export function parseGroup(group: NonNullable<SlashMenuItem['group']>) {
|
||||
return [
|
||||
parseInt(group.split('_')[0]),
|
||||
group.split('_')[1].split('@')[0],
|
||||
parseInt(group.split('@')[1]),
|
||||
] as const;
|
||||
}
|
||||
|
||||
function itemCompareFn(a: SlashMenuItem, b: SlashMenuItem) {
|
||||
if (a.group === undefined && b.group === undefined) return 0;
|
||||
if (a.group === undefined) return -1;
|
||||
if (b.group === undefined) return 1;
|
||||
|
||||
const [aGroupIndex, aGroupName, aItemIndex] = parseGroup(a.group);
|
||||
const [bGroupIndex, bGroupName, bItemIndex] = parseGroup(b.group);
|
||||
if (isNaN(aGroupIndex)) return -1;
|
||||
if (isNaN(bGroupIndex)) return 1;
|
||||
if (aGroupIndex < bGroupIndex) return -1;
|
||||
if (aGroupIndex > bGroupIndex) return 1;
|
||||
|
||||
if (aGroupName !== bGroupName) return aGroupName.localeCompare(bGroupName);
|
||||
|
||||
if (isNaN(aItemIndex)) return -1;
|
||||
if (isNaN(bItemIndex)) return 1;
|
||||
|
||||
return aItemIndex - bItemIndex;
|
||||
}
|
||||
|
||||
export function buildSlashMenuItems(
|
||||
items: SlashMenuItem[],
|
||||
context: SlashMenuContext,
|
||||
transform?: (item: SlashMenuItem) => SlashMenuItem
|
||||
): SlashMenuItem[] {
|
||||
if (transform) items = items.map(transform);
|
||||
|
||||
const result = items
|
||||
.filter(item => (item.when ? item.when(context) : true))
|
||||
.sort(itemCompareFn)
|
||||
.map(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return {
|
||||
...item,
|
||||
subMenu: buildSlashMenuItems(item.subMenu, context),
|
||||
};
|
||||
} else {
|
||||
return { ...item };
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mergeSlashMenuConfigs(
|
||||
configs: Map<string, SlashMenuConfig>
|
||||
): SlashMenuConfig {
|
||||
return {
|
||||
items: ctx =>
|
||||
Array.from(configs.values()).flatMap(({ items }) =>
|
||||
typeof items === 'function' ? items(ctx) : items
|
||||
),
|
||||
disableWhen: ctx =>
|
||||
configs
|
||||
.values()
|
||||
.map(({ disableWhen }) => disableWhen?.(ctx) ?? false)
|
||||
.some(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDate(date: Date) {
|
||||
// yyyy-mm-dd
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const strTime = `${year}-${month}-${day}`;
|
||||
return strTime;
|
||||
}
|
||||
|
||||
export function formatTime(date: Date) {
|
||||
// mm-dd hh:mm
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const strTime = `${month}-${day} ${hours}:${minutes}`;
|
||||
return strTime;
|
||||
}
|
||||
184
blocksuite/affine/widgets/slash-menu/src/widget.ts
Normal file
184
blocksuite/affine/widgets/slash-menu/src/widget.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { UIEventStateContext } from '@blocksuite/std';
|
||||
import { TextSelection, WidgetComponent } from '@blocksuite/std';
|
||||
import { InlineEditor } from '@blocksuite/std/inline';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
import { AFFINE_SLASH_MENU_TRIGGER_KEY } from './consts';
|
||||
import { SlashMenuExtension } from './extensions';
|
||||
import { SlashMenu } from './slash-menu-popover';
|
||||
import type { SlashMenuConfig, SlashMenuContext, SlashMenuItem } from './types';
|
||||
import { buildSlashMenuItems } from './utils';
|
||||
let globalAbortController = new AbortController();
|
||||
|
||||
function closeSlashMenu() {
|
||||
globalAbortController.abort();
|
||||
}
|
||||
|
||||
const showSlashMenu = debounce(
|
||||
({
|
||||
context,
|
||||
config,
|
||||
container = document.body,
|
||||
abortController = new AbortController(),
|
||||
configItemTransform,
|
||||
}: {
|
||||
context: SlashMenuContext;
|
||||
config: SlashMenuConfig;
|
||||
container?: HTMLElement;
|
||||
abortController?: AbortController;
|
||||
configItemTransform: (item: SlashMenuItem) => SlashMenuItem;
|
||||
}) => {
|
||||
globalAbortController = abortController;
|
||||
const disposables = new DisposableGroup();
|
||||
abortController.signal.addEventListener('abort', () =>
|
||||
disposables.dispose()
|
||||
);
|
||||
|
||||
const inlineEditor = getInlineEditorByModel(context.std, context.model);
|
||||
if (!inlineEditor) return;
|
||||
const slashMenu = new SlashMenu(inlineEditor, abortController);
|
||||
disposables.add(() => slashMenu.remove());
|
||||
slashMenu.context = context;
|
||||
slashMenu.items = buildSlashMenuItems(
|
||||
typeof config.items === 'function' ? config.items(context) : config.items,
|
||||
context,
|
||||
configItemTransform
|
||||
);
|
||||
|
||||
// FIXME(Flrande): It is not a best practice,
|
||||
// but merely a temporary measure for reusing previous components.
|
||||
// Mount
|
||||
container.append(slashMenu);
|
||||
return slashMenu;
|
||||
},
|
||||
100,
|
||||
{ leading: true }
|
||||
);
|
||||
|
||||
export class AffineSlashMenuWidget extends WidgetComponent {
|
||||
private readonly _getInlineEditor = (
|
||||
evt: KeyboardEvent | CompositionEvent
|
||||
) => {
|
||||
if (evt.target instanceof HTMLElement) {
|
||||
const editor = (
|
||||
evt.target.closest('.inline-editor') as {
|
||||
inlineEditor?: AffineInlineEditor;
|
||||
}
|
||||
)?.inlineEditor;
|
||||
if (editor instanceof InlineEditor) {
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
const textSelection = this.host.selection.find(TextSelection);
|
||||
if (!textSelection) return;
|
||||
|
||||
const model = this.host.doc.getBlock(textSelection.blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
return getInlineEditorByModel(this.std, model);
|
||||
};
|
||||
|
||||
private readonly _handleInput = (
|
||||
inlineEditor: InlineEditor,
|
||||
isCompositionEnd: boolean
|
||||
) => {
|
||||
const inlineRangeApplyCallback = (callback: () => void) => {
|
||||
// the inline ranged updated in compositionEnd event before this event callback
|
||||
if (isCompositionEnd) {
|
||||
callback();
|
||||
} else {
|
||||
const subscription = inlineEditor.slots.inlineRangeSync.subscribe(
|
||||
() => {
|
||||
subscription.unsubscribe();
|
||||
callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.block?.model.flavour !== 'affine:page') {
|
||||
console.error('SlashMenuWidget should be used in RootBlock');
|
||||
return;
|
||||
}
|
||||
|
||||
inlineRangeApplyCallback(() => {
|
||||
const textSelection = this.host.selection.find(TextSelection);
|
||||
if (!textSelection) return;
|
||||
|
||||
const block = this.host.view.getBlock(textSelection.blockId);
|
||||
if (!block) return;
|
||||
const model = block.model;
|
||||
|
||||
if (this.config.disableWhen?.({ model, std: this.std })) return;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
|
||||
const textPoint = inlineEditor.getTextPoint(inlineRange.index);
|
||||
if (!textPoint) return;
|
||||
|
||||
const [leafStart, offsetStart] = textPoint;
|
||||
|
||||
const text = leafStart.textContent
|
||||
? leafStart.textContent.slice(0, offsetStart)
|
||||
: '';
|
||||
|
||||
if (!text.endsWith(AFFINE_SLASH_MENU_TRIGGER_KEY)) return;
|
||||
|
||||
closeSlashMenu();
|
||||
showSlashMenu({
|
||||
context: {
|
||||
model,
|
||||
std: this.std,
|
||||
},
|
||||
config: this.config,
|
||||
configItemTransform: this.configItemTransform,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onCompositionEnd = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event as CompositionEvent;
|
||||
|
||||
if (event.data !== AFFINE_SLASH_MENU_TRIGGER_KEY) return;
|
||||
|
||||
const inlineEditor = this._getInlineEditor(event);
|
||||
if (!inlineEditor) return;
|
||||
|
||||
this._handleInput(inlineEditor, true);
|
||||
};
|
||||
|
||||
private readonly _onKeyDown = (ctx: UIEventStateContext) => {
|
||||
const eventState = ctx.get('keyboardState');
|
||||
const event = eventState.raw;
|
||||
|
||||
const key = event.key;
|
||||
|
||||
if (event.isComposing || key !== AFFINE_SLASH_MENU_TRIGGER_KEY) return;
|
||||
|
||||
const inlineEditor = this._getInlineEditor(event);
|
||||
if (!inlineEditor) return;
|
||||
|
||||
this._handleInput(inlineEditor, false);
|
||||
};
|
||||
|
||||
get config() {
|
||||
return this.std.get(SlashMenuExtension).config;
|
||||
}
|
||||
|
||||
// TODO(@L-Sun): Remove this when moving each config item to corresponding blocks
|
||||
// This is a temporary way for patching the slash menu config
|
||||
configItemTransform: (item: SlashMenuItem) => SlashMenuItem = item => item;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// this.handleEvent('beforeInput', this._onBeforeInput);
|
||||
this.handleEvent('keyDown', this._onKeyDown);
|
||||
this.handleEvent('compositionEnd', this._onCompositionEnd);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user