Skip to content

Commit

Permalink
feat: support more move case
Browse files Browse the repository at this point in the history
analysis shift + cursor behaviour and imp the logic

others:
 - define some useful types
 - rename some functions
 - document support
 - fix token count test
  • Loading branch information
sailist committed Jan 14, 2025
1 parent 174c213 commit e84e4b4
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 69 deletions.
67 changes: 63 additions & 4 deletions document/components/playboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,79 @@ interface EditableDivProps {
stride?: 'char' | 'word' | 'softline';
}

export const EditableDiv: React.FC<EditableDivProps> = ({
export const EditableManually: React.FC<EditableDivProps> = ({
initialContent = '',
stride = 'char',
}) => {
const divRef = useRef<HTMLDivElement>(null);
const displayRef = useRef<HTMLDivElement>(null);
const anchorRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<AnchorEditor | null>(null);

useEffect(() => {
if (!divRef.current) {
return;
}
// 使用 AnchorEditor 替代 AnchorQuery
editorRef.current = new AnchorEditor({}, divRef.current);

// 添加键盘事件处理
const handleKeyDown = (e: KeyboardEvent) => {
if (!editorRef.current) return;

// 处理方向键
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
e.stopPropagation();

const direction = e.key === 'ArrowLeft' ? 'left' : 'right';
let ret = editorRef.current.moveRangeTo({
direction,
stride,
shift: e.shiftKey,
});

if (ret.error) {
console.error('moveAnchorTo failed:', ret.error);
return;
}

// 更新显示信息
if (!displayRef.current || !anchorRef.current) return;
const range = document.getSelection()?.getRangeAt(0);
if (!range) return;

const offset = editorRef.current._getOffsetByAnchor({
container: range.startContainer,
offset: range.startOffset,
});

displayRef.current.innerText = `${range.startContainer.nodeName}, ${offset}`;
anchorRef.current.innerText = anchorToStrong({
container: range.startContainer,
offset: range.startOffset,
});
}
};

divRef.current.addEventListener('keydown', handleKeyDown);

// 清理事件监听器
return () => {
divRef.current?.removeEventListener('keydown', handleKeyDown);
};
}, [stride]); // 添加 stride 作为依赖

useEffect(() => {
if (!divRef.current) {
return;
}
// 假设 register 函数存在并需要在组件挂载时调用
const editor = new AnchorQuery({}, divRef.current);
console.log(editor);
divRef.current.addEventListener('keyup', () => {
divRef.current.addEventListener('keyup', (e) => {
e.preventDefault();
e.stopPropagation();
if (!displayRef.current) {
return;
}
Expand Down Expand Up @@ -77,7 +136,7 @@ export const EditableDiv: React.FC<EditableDivProps> = ({
};

// ... existing code ...
export const EditablePlay: React.FC<EditableDivProps> = ({
export const EditablePlayable: React.FC<EditableDivProps> = ({
initialContent = '',
stride = 'char',
}) => {
Expand Down Expand Up @@ -141,7 +200,7 @@ export const EditablePlay: React.FC<EditableDivProps> = ({
}
const editor = editorRef.current;
if (editor) {
const ret = editor.moveStartAnchorTo({
const ret = editor.moveRangeTo({
direction: 'right',
stride: stride,
});
Expand Down
21 changes: 16 additions & 5 deletions document/docs/hello.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Start

import { EditableDiv, EditablePlay } from '../components/playboard';
import { EditableManually, EditablePlayable } from '../components/playboard';
import { version, buildTime } from "./version.json"


Expand All @@ -14,17 +14,28 @@ Write something to build your own docs! 🎁
</div>


<EditableDiv
initialContent="在这里开始编辑..."
<EditableManually
initialContent="在 这里 开始 编辑..."
stride="char"
/>../components/playboard


<EditablePlay

<EditableManually
initialContent="在 这里 开始 编辑..."
stride="word"
/>../components/playboard


/>../components/playboard


<EditablePlayable
initialContent="在 这里 开始 编辑..."
stride="char"
/>../components/playboard

<EditablePlay
<EditablePlayable
initialContent="在 这里 开始 编辑..."
stride="word"
/>../components/playboard
194 changes: 158 additions & 36 deletions src/editor.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import type {
Anchor,
MoveAnchorPayload,
NeighborResult,
RangeDirection,
StatefulAnchorEditorInterface,
Step,
} from './interface';
import { AnchorQuery } from './query';
import { AnchorQuery, type QueryConfig } from './query';

export interface EditorConfig {
defaultQuery: AnchorQuery;
// rules: [];
}

/**
*
* paragraph_rule: (node: Node) => boolean | { clazzName?: string, tagName?: string };
* rich_rule: (node: Node) => boolean | { clazzName?: string, tagName?: string };
*
* AnchorEditor(config, root)
*
*/


export class AnchorEditor
extends AnchorQuery
implements StatefulAnchorEditorInterface
{
implements StatefulAnchorEditorInterface {
lastMoveDirection: 'left' | 'right' | 'up' | 'down' | 'none' = 'none';
lastMoveAnchor: 'start' | 'end' | 'none' = 'none';
moveAnchor: RangeDirection = 'start';
fixedAnchor: RangeDirection = 'start';

// constructor(config: LocConfig, root: HTMLElement) {
// super(config, root);
// }
Expand All @@ -20,27 +38,20 @@ export class AnchorEditor
if (selection) {
selection.setPosition(anchor.container, anchor.offset);
this.lastMoveDirection = 'none';
this.lastMoveAnchor = 'none';
return anchor;
}
return null;
}
resetStartAnchor(anchor: Anchor): Anchor | null {
const range = document.getSelection()?.getRangeAt(0);
if (range) {
range.setStart(anchor.container, anchor.offset);
this.lastMoveDirection = 'none';
this.lastMoveAnchor = 'start';
this.moveAnchor = 'start';
this.fixedAnchor = 'start';
return anchor;
}
return null;
}

resetEndAnchor(anchor: Anchor): Anchor | null {
const range = document.getSelection()?.getRangeAt(0);
if (range) {
range.setEnd(anchor.container, anchor.offset);
range.collapse();
this.lastMoveDirection = 'none';
this.lastMoveAnchor = 'end';
this.moveAnchor = 'end';
return anchor;
}
return null;
Expand All @@ -52,43 +63,154 @@ export class AnchorEditor
range.setStart(start.container, start.offset);
range.setEnd(end.container, end.offset);
this.lastMoveDirection = 'none';
this.lastMoveAnchor = 'none';
this.moveAnchor = 'start';
this.fixedAnchor = 'start';
return true;
}
return false;
}

moveEndAnchorTo(step: Step): NeighborResult {
isCollapsed(): boolean {
const range = document.getSelection()?.getRangeAt(0);
if (!range) {
return false;
}
return range.collapsed;
}

collapse(rangeDirection: RangeDirection): Anchor | null {
const range = document.getSelection()?.getRangeAt(0);
if (range) {
const result = this.getHorizontalAnchor({
anchor: { container: range.endContainer, offset: range.endOffset },
step: step,
});
if (result.next) {
this.resetEndAnchor(result.next);
this.lastMoveDirection = step.direction;
this.lastMoveAnchor = 'end';
return result;
}
range.collapse(rangeDirection === "start");
return {
container: range.startContainer,
offset: range.startOffset,
};
}
throw new Error('no selection');
return null;
}

moveStartAnchorTo(step: Step): NeighborResult {
setAnchor(anchor: Anchor, payload: MoveAnchorPayload): Anchor | null {
const range = document.getSelection()?.getRangeAt(0);
if (range) {
if (payload.rangeDirection === "both") {
range.setStart(anchor.container, anchor.offset);
range.setEnd(anchor.container, anchor.offset);
} else if (payload.rangeDirection === "start") {
range.setStart(anchor.container, anchor.offset);
} else {
range.setEnd(anchor.container, anchor.offset);
}


if (payload.collapsed) {
range.collapse(payload.collapsed === "start");
}
// range.setStart(anchor.container, anchor.offset);
// range.setEnd(anchor.container, anchor.offset);
// if (payload.rangeDirection === "start") {
// const end = {
// container: range.endContainer,
// offset: range.endOffset,
// };
// } else {
// const start = {
// container: range.startContainer,
// offset: range.startOffset,
// };
// range.setStart(start.container, start.offset);
// range.setEnd(anchor.container, anchor.offset);
// }
return anchor;
}
return null;
}

/**
* moveAnchorTo only considered keyboard event:
*
* it set the selection by control the moveAnchor and fixedAnchor
* 1. selection is collapsed
* - moveAnchor = caretAnchor = 'start'
*
* 2. selection is not collapsed
* - if shift direction is left, moveAnchor = 'start', fixedAnchor = 'end'
* - if shift direction is right, moveAnchor = 'end', fixedAnchor = 'start'
*
*/
moveRangeTo(step: Step): NeighborResult {
const range = document.getSelection()?.getRangeAt(0);
if (!range) {
throw new Error('no selection');
}

if (step.direction === 'left' || step.direction === 'right') {

if (!step.shift && !this.isCollapsed()) {
this.collapse(step.direction === 'left' ? 'start' : 'end');
this.fixedAnchor = this.moveAnchor;
return this.getHorizontalAnchor({
anchor: {
container: this.moveAnchor === 'start' ? range.startContainer : range.endContainer,
offset: this.moveAnchor === 'start' ? range.startOffset : range.endOffset
},
step: {
direction: step.direction,
stride: 'none',
},
});
}

const result = this.getHorizontalAnchor({
anchor: { container: range.startContainer, offset: range.startOffset },
anchor: {
container: this.moveAnchor === 'start' ? range.startContainer : range.endContainer,
offset: this.moveAnchor === 'start' ? range.startOffset : range.endOffset
},
step: step,
});
if (result.next) {
this.resetStartAnchor(result.next);
this.lastMoveDirection = step.direction;
this.lastMoveAnchor = 'start';
if (!result.next) {
return result;
}

let payload: MoveAnchorPayload = {
rangeDirection: this.moveAnchor,
};
if (step.shift) {
if (this.moveAnchor !== this.fixedAnchor) {
// 1. if shift and previous is also shift
// only move the moveAnchor
} else {
// 2. if shift and previous is not shift
// move moveAnchor and keep the fixedAnchor in the same anchor
this.moveAnchor = step.direction === 'left' ? 'start' : 'end';
this.fixedAnchor = step.direction === 'left' ? 'end' : 'start';

payload.rangeDirection = this.moveAnchor;
}
} else {
if (!this.isCollapsed()) {
// 3. if not shift and previous is shift
// collapse the selection in the moveAnchor and make fixedAnchor align with moveAnchor
this.fixedAnchor = this.moveAnchor;
} else {
// 4. if not shift and previous is not shift
// move moveAnchor and fixedAnchor in the same direction
this.moveAnchor = step.direction === 'left' ? 'start' : 'end';
this.fixedAnchor = step.direction === 'left' ? 'start' : 'end';
}
payload.collapsed = this.moveAnchor === 'start' ? 'start' : 'end';
}
this.setAnchor(result.next, payload);

if (this.isCollapsed()) {
this.fixedAnchor = step.direction === 'left' ? 'start' : 'end';
this.moveAnchor = this.fixedAnchor;
}

return result;
} else {
throw new Error(`invalid direction ${step.direction}`);
}
throw new Error('no selection');
}
}

Loading

0 comments on commit e84e4b4

Please sign in to comment.