Files
AFFiNE/blocksuite/framework/std/src/selection/text.ts
Saul-Mirone 7eb6b268a6 fix(editor): auto focus between tab switch (#12572)
Closes: BS-2290

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved focus behavior when switching between tabs to prevent unwanted automatic focusing of the content-editable area.
  - Enhanced selection clearing to avoid unnecessary blurring when the main editable element is already focused.
  - Refined focus checks in tests to specifically target contenteditable elements, ensuring more accurate validation of focus behavior.
  - Adjusted test assertions for block selection to be less strict and removed redundant blur operations for smoother test execution.
  - Updated toolbar dropdown closing method to use keyboard interaction for better reliability.
- **New Features**
  - Added a recoverable property to selection types, improving selection state management and recovery.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 13:38:02 +00:00

118 lines
2.5 KiB
TypeScript

import { BaseSelection, SelectionExtension } from '@blocksuite/store';
import z from 'zod';
export type TextRangePoint = {
blockId: string;
index: number;
length: number;
};
export type TextSelectionProps = {
from: TextRangePoint;
to: TextRangePoint | null;
reverse?: boolean;
};
const TextSelectionSchema = z.object({
from: z.object({
blockId: z.string(),
index: z.number(),
length: z.number(),
}),
to: z
.object({
blockId: z.string(),
index: z.number(),
length: z.number(),
})
.nullable(),
// The `optional()` is for backward compatibility,
// since `reverse` may not exist in remote selection.
reverse: z.boolean().optional(),
});
export class TextSelection extends BaseSelection {
static override group = 'note';
static override type = 'text';
static override recoverable = true;
from: TextRangePoint;
reverse: boolean;
to: TextRangePoint | null;
get end(): TextRangePoint {
return this.reverse ? this.from : (this.to ?? this.from);
}
get start(): TextRangePoint {
return this.reverse ? (this.to ?? this.from) : this.from;
}
constructor({ from, to, reverse }: TextSelectionProps) {
super({
blockId: from.blockId,
});
this.from = from;
this.to = this._equalPoint(from, to) ? null : to;
this.reverse = !!reverse;
}
static override fromJSON(json: Record<string, unknown>): TextSelection {
const result = TextSelectionSchema.parse(json);
return new TextSelection(result);
}
private _equalPoint(
a: TextRangePoint | null,
b: TextRangePoint | null
): boolean {
if (a && b) {
return (
a.blockId === b.blockId && a.index === b.index && a.length === b.length
);
}
return a === b;
}
empty(): boolean {
return !!this.to;
}
override equals(other: BaseSelection): boolean {
if (other instanceof TextSelection) {
return (
this.blockId === other.blockId &&
this._equalPoint(other.from, this.from) &&
this._equalPoint(other.to, this.to)
);
}
return false;
}
isCollapsed(): boolean {
return this.to === null && this.from.length === 0;
}
isInSameBlock(): boolean {
return this.to === null || this.from.blockId === this.to.blockId;
}
override toJSON(): Record<string, unknown> {
return {
type: 'text',
from: this.from,
to: this.to,
reverse: this.reverse,
};
}
}
export const TextSelectionExtension = SelectionExtension(TextSelection);