Skip to content

Commit

Permalink
fix(blocks): ime bugs of slash menu and at menu
Browse files Browse the repository at this point in the history
  • Loading branch information
L-Sun committed Sep 24, 2024
1 parent 2b422f2 commit 8384188
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 106 deletions.
12 changes: 7 additions & 5 deletions packages/blocks/src/_common/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getQuery(
interface ObserverParams {
target: HTMLElement;
signal: AbortSignal;
onInput?: () => void;
onInput?: (isComposition: boolean) => void;
onDelete?: () => void;
onMove?: (step: 1 | -1) => void;
onConfirm?: () => void;
Expand All @@ -60,6 +60,8 @@ export const createKeydownObserver = ({
interceptor = (_, next) => next(),
}: ObserverParams) => {
const keyDownListener = (e: KeyboardEvent) => {
if (e.key === 'Process' || e.isComposing) return;

if (e.defaultPrevented) return;

if (isControlledKeyboardEvent(e)) {
Expand Down Expand Up @@ -107,10 +109,10 @@ export const createKeydownObserver = ({

if (
// input abc, 123, etc.
(!isControlledKeyboardEvent(e) && e.key.length === 1) ||
e.isComposing
!isControlledKeyboardEvent(e) &&
e.key.length === 1
) {
onInput?.();
onInput?.(false);
return;
}

Expand Down Expand Up @@ -184,7 +186,7 @@ export const createKeydownObserver = ({
target.addEventListener('paste', () => onDelete?.(), { signal });

// Fix composition input
target.addEventListener('input', () => onInput?.(), { signal });
target.addEventListener('compositionend', () => onInput?.(true), { signal });
};

/**
Expand Down
68 changes: 35 additions & 33 deletions packages/blocks/src/root-block/widgets/linked-doc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,45 @@ export interface LinkedWidgetConfig {
export class AffineLinkedDocWidget extends WidgetComponent {
private _abortController: AbortController | null = null;

private _getInlineEditor = (evt: KeyboardEvent | CompositionEvent) => {
if (evt.target instanceof HTMLElement) {
const editor = (
evt.target.closest('.can-link-doc > .inline-editor') as {
inlineEditor?: AffineInlineEditor;
}
)?.inlineEditor;
if (editor instanceof InlineEditor) {
return editor;
}
}

const text = this.host.selection.value.find(selection =>
selection.is('text')
);
if (!text) return;

const model = this.host.doc.getBlockById(text.blockId);
if (!model) return;

if (matchFlavours(model, this.config.ignoreBlockTypes)) {
return;
}

return getInlineEditorByModel(this.host, model);
};

private _onCompositionEnd = (ctx: UIEventStateContext) => {
const event = ctx.get('defaultState').event as CompositionEvent;

const key = event.data;

if (
!this.config.triggerKeys.some(triggerKey =>
triggerKey.includes(event.data)
)
!key ||
!this.config.triggerKeys.some(triggerKey => triggerKey.includes(key))
)
return;

const inlineEditor = this.getInlineEditor(event);
const inlineEditor = this._getInlineEditor(event);
if (!inlineEditor) return;

this._handleInput(inlineEditor, true);
Expand All @@ -68,10 +96,11 @@ export class AffineLinkedDocWidget extends WidgetComponent {
)
return;

const inlineEditor = this.getInlineEditor(event);
const inlineEditor = this._getInlineEditor(event);
if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;

if (inlineRange.length > 0) {
// When select text and press `[[` should not trigger transform,
// since it will break the bracket complete.
Expand All @@ -82,33 +111,6 @@ export class AffineLinkedDocWidget extends WidgetComponent {
this._handleInput(inlineEditor, false);
};

private getInlineEditor = (evt: KeyboardEvent | CompositionEvent) => {
if (evt.target instanceof HTMLElement) {
const editor = (
evt.target.closest('.can-link-doc > .inline-editor') as {
inlineEditor?: AffineInlineEditor;
}
)?.inlineEditor;
if (editor instanceof InlineEditor) {
return editor;
}
}

const text = this.host.selection.value.find(selection =>
selection.is('text')
);
if (!text) return;

const model = this.host.doc.getBlockById(text.blockId);
if (!model) return;

if (matchFlavours(model, this.config.ignoreBlockTypes)) {
return;
}

return getInlineEditorByModel(this.host, model);
};

showLinkedDocPopover = (
inlineEditor: AffineInlineEditor,
triggerKey: string
Expand Down Expand Up @@ -216,7 +218,7 @@ export class AffineLinkedDocWidget extends WidgetComponent {
index: startIdxBeforeMatchKey + primaryTriggerKey.length,
length: 0,
});
inlineRangeApplyCallback(() => {
inlineEditor.slots.inlineRangeSync.once(() => {
this.showLinkedDocPopover(inlineEditor, primaryTriggerKey);
});
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@ export class LinkedDocPopover extends WithDisposable(LitElement) {
createKeydownObserver({
target: eventSource,
signal: this.abortController.signal,
onInput: () => {
onInput: isComposition => {
this._activatedItemIndex = 0;
this.inlineEditor.slots.renderComplete.once(this._updateLinkedDocGroup);
if (isComposition) {
this._updateLinkedDocGroup().catch(console.error);
} else {
this.inlineEditor.slots.renderComplete.once(
this._updateLinkedDocGroup
);
}
},
onPaste: () => {
this._activatedItemIndex = 0;
Expand Down
164 changes: 118 additions & 46 deletions packages/blocks/src/root-block/widgets/slash-menu/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import type { UIEventStateContext } from '@blocksuite/block-std';

import { getInlineEditorByModel } from '@blocksuite/affine-components/rich-text';
import {
getCurrentNativeRange,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
type AffineInlineEditor,
getInlineEditorByModel,
} from '@blocksuite/affine-components/rich-text';
import { getCurrentNativeRange } from '@blocksuite/affine-shared/utils';
import { WidgetComponent } from '@blocksuite/block-std';
import {
assertExists,
assertType,
debounce,
DisposableGroup,
throttle,
} from '@blocksuite/global/utils';
import { InlineEditor } from '@blocksuite/inline';

import type { RootBlockComponent } from '../../types.js';

Expand Down Expand Up @@ -47,19 +49,20 @@ function closeSlashMenu() {
const showSlashMenu = debounce(
({
context,
range,
container = document.body,
abortController = new AbortController(),
config,
triggerKey,
}: {
context: SlashMenuContext;
range: Range;
container?: HTMLElement;
abortController?: AbortController;
config: SlashMenuStaticConfig;
triggerKey: string;
}) => {
const curRange = getCurrentNativeRange();
if (!curRange) return;

globalAbortController = abortController;
const disposables = new DisposableGroup();
abortController.signal.addEventListener('abort', () =>
Expand All @@ -84,7 +87,7 @@ const showSlashMenu = debounce(
slashMenuElement,
'You should render the slash menu node even if no position'
);
const position = getPopperPosition(slashMenuElement, range);
const position = getPopperPosition(slashMenuElement, curRange);
slashMenu.updatePosition(position);
}, 10);

Expand All @@ -106,58 +109,125 @@ export const AFFINE_SLASH_MENU_WIDGET = 'affine-slash-menu-widget';
export class AffineSlashMenuWidget extends WidgetComponent {
static DEFAULT_CONFIG = defaultSlashMenuConfig;

private _onBeforeInput = (ctx: UIEventStateContext) => {
const eventState = ctx.get('defaultState');
const event = eventState.event as InputEvent;

const triggerKey = event.data;
if (!triggerKey || !this.config.triggerKeys.includes(triggerKey)) return;
private _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('text');
if (!textSelection) return;

const block = this.host.doc.getBlock(textSelection.blockId);
assertExists(block);

const { model } = block;
const model = this.host.doc.getBlock(textSelection.blockId)?.model;
if (!model) return;

if (matchFlavours(model, this.config.ignoreBlockTypes)) return;
return getInlineEditorByModel(this.host, model);
};

const inlineEditor = getInlineEditorByModel(this.host, model);
if (!inlineEditor) return;
private _handleInput = (
inlineEditor: InlineEditor,
isCompositionEnd: boolean
) => {
const inlineRangeApplyCallback = (callback: () => void) => {
// the inline ranged updated in compositionEnd event before this event callback
if (isCompositionEnd) callback();
else inlineEditor.slots.inlineRangeSync.once(callback);
};

const rootComponent = this.block;
if (rootComponent.model.flavour !== 'affine:page') {
console.error('SlashMenuWidget should be used in RootBlock');
return;
}
assertType<RootBlockComponent>(rootComponent);

const config: SlashMenuStaticConfig = {
...this.config,
items: filterEnabledSlashMenuItems(this.config.items, {
model,
rootComponent: rootComponent as RootBlockComponent,
}),
};
inlineRangeApplyCallback(() => {
const textSelection = this.host.selection.find('text');
if (!textSelection) return;

const model = this.host.doc.getBlock(textSelection.blockId)?.model;
if (!model) 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)
: '';

const matchedKey = this.config.triggerKeys.find(triggerKey =>
text.endsWith(triggerKey)
);
if (!matchedKey) return;

const config: SlashMenuStaticConfig = {
...this.config,
items: filterEnabledSlashMenuItems(this.config.items, {
model,
rootComponent,
}),
};

closeSlashMenu();
showSlashMenu({
context: {
model,
rootComponent,
},
triggerKey: matchedKey,
config,
});
});
};

private _onCompositionEnd = (ctx: UIEventStateContext) => {
const event = ctx.get('defaultState').event as CompositionEvent;

if (
!this.config.triggerKeys.some(triggerKey =>
triggerKey.includes(event.data)
)
)
return;

const inlineEditor = this._getInlineEditor(event);
if (!inlineEditor) return;

this._handleInput(inlineEditor, true);
};

private _onKeyDown = (ctx: UIEventStateContext) => {
const eventState = ctx.get('keyboardState');
const event = eventState.raw;

const key = event.key;

// check event is not composing
if (
key === undefined || // in mac os, the key may be undefined
key === 'Process' ||
event.isComposing
)
return;

if (!this.config.triggerKeys.some(triggerKey => triggerKey.includes(key)))
return;

const inlineEditor = this._getInlineEditor(event);
if (!inlineEditor) return;

inlineEditor
.waitForUpdate()
.then(() => {
const curRange = getCurrentNativeRange();
if (!curRange) return;

closeSlashMenu();
showSlashMenu({
context: {
model,
rootComponent: rootComponent as RootBlockComponent,
},
range: curRange,
triggerKey,
config,
});
})
.catch(console.error);
this._handleInput(inlineEditor, false);
};

config = AffineSlashMenuWidget.DEFAULT_CONFIG;
Expand All @@ -170,7 +240,9 @@ export class AffineSlashMenuWidget extends WidgetComponent {
return;
}

this.handleEvent('beforeInput', this._onBeforeInput);
// this.handleEvent('beforeInput', this._onBeforeInput);
this.handleEvent('keyDown', this._onKeyDown);
this.handleEvent('compositionEnd', this._onCompositionEnd);
}
}

Expand Down
Loading

0 comments on commit 8384188

Please sign in to comment.