Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .docker/selfhost/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@
"description": "Whether require email verification before accessing restricted resources(not implemented).\n@default true",
"default": true
},
"newAccountShareActionDelay": {
"newAccountActionDelay": {
"type": "number",
"description": "Minimum account age in seconds before new accounts can invite members or create share links.\n@default 86400",
"description": "Minimum account age in seconds before new accounts can invite members, create invite links, or publish documents. Set to 0 to disable.\n@default 86400",
"default": 86400
},
"trustedCloudflareHeaders": {
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion blocksuite/affine/blocks/root/src/page/page-root-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getScrollContainer,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { IS_ANDROID } from '@blocksuite/global/env';
import { Point } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std';
import { BlockComponent, BlockSelection, TextSelection } from '@blocksuite/std';
Expand Down Expand Up @@ -413,7 +414,10 @@ export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
return !(isNote && displayOnEdgeless);
});

this.contentEditable = String(!this.store.readonly$.value);
// Android IMEs can target this outer editable root instead of a block's
// inline editor, leaving composition text in the DOM without committing it
// to the document model. Keep only the block editors editable on Android.
this.contentEditable = String(!this.store.readonly$.value && !IS_ANDROID);

return html`
<div class="affine-page-root-block-container">${children} ${widgets}</div>
Expand Down
18 changes: 10 additions & 8 deletions blocksuite/affine/rich-text/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ export function getPrefixText(inlineEditor: InlineEditor) {
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange || inlineRange.length > 0) return '';

const nearestLineBreakIndex = inlineEditor.yTextString
.slice(0, inlineRange.index)
.lastIndexOf('\n');
const prefixText = inlineEditor.yTextString.slice(
nearestLineBreakIndex + 1,
inlineRange.index
);
return prefixText;
const maxMarkdownPrefixLength = 512;
const prefixStart = Math.max(0, inlineRange.index - maxMarkdownPrefixLength);
const yTextString = inlineEditor.yTextString;
const prefixWindow = yTextString.slice(prefixStart, inlineRange.index);
const nearestLineBreakIndex = prefixWindow.lastIndexOf('\n');
if (nearestLineBreakIndex === -1 && prefixStart > 0) return '';

return nearestLineBreakIndex === -1
? prefixWindow
: prefixWindow.slice(nearestLineBreakIndex + 1);
}
88 changes: 86 additions & 2 deletions blocksuite/framework/std/src/__tests__/keymap.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';

import { bindKeymap } from '../event/keymap.js';
import { UIEventState, UIEventStateContext } from '../event/base.js';
import { androidBindKeymapPatch, bindKeymap } from '../event/keymap.js';

const createKeyboardEvent = (options: {
key: string;
Expand Down Expand Up @@ -117,3 +118,86 @@ describe('bindKeymap', () => {
expect(handled).toBe(false);
});
});

describe('androidBindKeymapPatch', () => {
const beforeInputCtx = (inputType: string) => {
const event = new InputEvent('beforeinput', {
inputType,
cancelable: true,
});
return { ctx: UIEventStateContext.from(new UIEventState(event)), event };
};

test('routes deleteContentBackward to the Backspace binding', () => {
const backspace = vi.fn(() => true);
const handler = androidBindKeymapPatch({ Backspace: backspace });
const { ctx } = beforeInputCtx('deleteContentBackward');

expect(handler(ctx)).toBe(true);
expect(backspace).toHaveBeenCalledOnce();
});

test('routes insertParagraph to the Enter binding', () => {
const enter = vi.fn((ctx: UIEventStateContext) => {
ctx.get('keyboardState').raw.preventDefault();
return true;
});
const handler = androidBindKeymapPatch({ Enter: enter });
const { ctx, event } = beforeInputCtx('insertParagraph');
const preventDefault = vi.spyOn(event, 'preventDefault');

expect(handler(ctx)).toBe(true);
expect(enter).toHaveBeenCalledOnce();
expect(preventDefault).toHaveBeenCalledOnce();
expect(ctx.get('keyboardState').raw.key).toBe('Enter');
expect(ctx.get('keyboardState').composing).toBe(false);
});

test('propagates preventDefault when the binding returns false', () => {
const backspace = vi.fn((ctx: UIEventStateContext) => {
ctx.get('keyboardState').raw.preventDefault();
return false;
});
const handler = androidBindKeymapPatch({ Backspace: backspace });
const { ctx, event } = beforeInputCtx('deleteContentBackward');

expect(handler(ctx)).toBe(false);
expect(event.defaultPrevented).toBe(true);
});

test('does nothing for insertParagraph without an Enter binding', () => {
const handler = androidBindKeymapPatch({ Backspace: vi.fn(() => true) });
const { ctx } = beforeInputCtx('insertParagraph');

expect(handler(ctx)).toBe(false);
expect(ctx.has('keyboardState')).toBe(false);
});

test('ignores non-input events', () => {
const enter = vi.fn(() => true);
const backspace = vi.fn(() => true);
const ctx = UIEventStateContext.from(
new UIEventState(new KeyboardEvent('keydown', { key: 'Enter' }))
);

expect(
androidBindKeymapPatch({ Enter: enter, Backspace: backspace })(ctx)
).toBeUndefined();
expect(enter).not.toHaveBeenCalled();
expect(backspace).not.toHaveBeenCalled();
});

test('ignores unrelated input types', () => {
const enter = vi.fn(() => true);
const backspace = vi.fn(() => true);
const handler = androidBindKeymapPatch({
Enter: enter,
Backspace: backspace,
});
const { ctx } = beforeInputCtx('insertText');

expect(handler(ctx)).toBe(false);
expect(enter).not.toHaveBeenCalled();
expect(backspace).not.toHaveBeenCalled();
});
});
38 changes: 32 additions & 6 deletions blocksuite/framework/std/src/event/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { base, keyName } from 'w3c-keyname';

import type { UIEventHandler } from './base.js';
import { KeyboardEventState } from './state/index.js';

function normalizeKeyName(name: string) {
const parts = name.split(/-(?!$)/);
Expand Down Expand Up @@ -127,13 +128,38 @@ export function androidBindKeymapPatch(
const event = ctx.get('defaultState').event;
if (!(event instanceof InputEvent)) return;

if (
event.inputType === 'deleteContentBackward' &&
'Backspace' in bindings
) {
return bindings['Backspace'](ctx);
const bindingName =
event.inputType === 'deleteContentBackward'
? 'Backspace'
: event.inputType === 'deleteContentForward'
? 'Delete'
: event.inputType === 'insertParagraph'
? 'Enter'
: undefined;
if (!bindingName || !(bindingName in bindings)) return false;

if (!ctx.has('keyboardState')) {
const keyboardEvent = new KeyboardEvent('keydown', {
key: bindingName,
code: bindingName,
cancelable: true,
});
Object.defineProperty(keyboardEvent, 'isComposing', {
configurable: true,
value: event.isComposing,
});
ctx.add(
new KeyboardEventState({
event: keyboardEvent,
composing: event.isComposing,
})
);
}

return false;
const handled = bindings[bindingName](ctx);
if (handled || ctx.get('keyboardState').raw.defaultPrevented) {
event.preventDefault();
}
return handled;
};
}
Loading
Loading