Skip to content

Commit fd2f795

Browse files
committed
test
1 parent 6019772 commit fd2f795

6 files changed

Lines changed: 457 additions & 0 deletions

File tree

packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ChangeSource } from 'roosterjs-content-model-dom';
22
import { checkAndInsertHorizontalLine } from './horizontalLine/checkAndInsertHorizontalLine';
33
import { createLink } from './link/createLink';
44
import { formatTextSegmentBeforeSelectionMarker, promoteLink } from 'roosterjs-content-model-api';
5+
import { isLastWordUrl } from './link/isLastWordUrl';
6+
import { isLetterFollowedByMarker } from './list/isLetterFollowedByMarker';
57
import { keyboardListTrigger } from './list/keyboardListTrigger';
68
import { transformFraction } from './numbers/transformFraction';
79
import { transformHyphen } from './hyphen/transformHyphen';
@@ -100,6 +102,37 @@ export class AutoFormatPlugin implements EditorPlugin {
100102
this.editor = null;
101103
}
102104

105+
private shouldHandleInputEventExclusively(editor: IEditor, event: EditorInputEvent) {
106+
const rawEvent = event.rawEvent;
107+
const selection = editor.getDOMSelection();
108+
if (
109+
rawEvent.inputType === 'insertText' &&
110+
selection &&
111+
selection.type === 'range' &&
112+
selection.range.collapsed &&
113+
rawEvent.data == ' '
114+
) {
115+
const previousText = selection.range.startContainer.textContent?.trim();
116+
if (!previousText) {
117+
return false;
118+
}
119+
120+
return isLastWordUrl(previousText) || isLetterFollowedByMarker(previousText);
121+
}
122+
return false;
123+
}
124+
125+
willHandleEventExclusively(event: PluginEvent) {
126+
if (this.editor) {
127+
console.log(event);
128+
switch (event.eventType) {
129+
case 'input':
130+
return this.shouldHandleInputEventExclusively(this.editor, event);
131+
}
132+
}
133+
return false;
134+
}
135+
103136
/**
104137
* Core method for a plugin. Once an event happens in editor, editor will call this
105138
* method of each plugin to handle the event as long as the event is not handled
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { matchLink } from 'roosterjs-content-model-api';
2+
3+
/**
4+
* @internal
5+
*/
6+
export const isLastWordUrl = (text: string): boolean => {
7+
if (!text) return false;
8+
9+
const words = text.trim().split(/\s+/);
10+
const lastWord = words[words.length - 1];
11+
12+
return !!matchLink(lastWord)?.normalizedUrl;
13+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @internal
3+
*/
4+
export const isLetterFollowedByMarker = (text: string): boolean => {
5+
if (!text || text.length !== 2) {
6+
return false;
7+
}
8+
9+
// Check if the string ends with ), . or -
10+
const lastChar = text[text.length - 1];
11+
if (lastChar !== ')' && lastChar !== '.' && lastChar !== '-') {
12+
return false;
13+
}
14+
15+
// Check if the first character is a single letter
16+
const firstChar = text[0];
17+
return /^[a-zA-Z]$/.test(firstChar);
18+
};

packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2483,4 +2483,241 @@ describe('Content Model Auto Format Plugin Test', () => {
24832483
);
24842484
});
24852485
});
2486+
2487+
describe('willHandleEventExclusively', () => {
2488+
let plugin: AutoFormatPlugin;
2489+
let mockEditor: IEditor;
2490+
let mockSelection: any;
2491+
2492+
beforeEach(() => {
2493+
mockSelection = {
2494+
type: 'range',
2495+
range: {
2496+
collapsed: true,
2497+
startContainer: {
2498+
textContent: '',
2499+
},
2500+
},
2501+
};
2502+
2503+
mockEditor = {
2504+
getDOMSelection: () => mockSelection,
2505+
} as any;
2506+
2507+
plugin = new AutoFormatPlugin({
2508+
autoBullet: true,
2509+
autoNumbering: true,
2510+
autoLink: true,
2511+
});
2512+
plugin.initialize(mockEditor);
2513+
});
2514+
2515+
afterEach(() => {
2516+
plugin.dispose();
2517+
});
2518+
2519+
it('should return true when previous text is a letter followed by marker', () => {
2520+
mockSelection.range.startContainer.textContent = 'a.';
2521+
2522+
const event: EditorInputEvent = {
2523+
eventType: 'input',
2524+
rawEvent: {
2525+
inputType: 'insertText',
2526+
data: ' ',
2527+
} as any,
2528+
};
2529+
2530+
const result = plugin.willHandleEventExclusively(event);
2531+
expect(result).toBe(true);
2532+
});
2533+
2534+
it('should return true when previous text contains a URL', () => {
2535+
mockSelection.range.startContainer.textContent = 'Visit https://example.com';
2536+
2537+
const event: EditorInputEvent = {
2538+
eventType: 'input',
2539+
rawEvent: {
2540+
inputType: 'insertText',
2541+
data: ' ',
2542+
} as any,
2543+
};
2544+
2545+
const result = plugin.willHandleEventExclusively(event);
2546+
expect(result).toBe(true);
2547+
});
2548+
2549+
it('should return false when input is not a space', () => {
2550+
mockSelection.range.startContainer.textContent = 'a.';
2551+
2552+
const event: EditorInputEvent = {
2553+
eventType: 'input',
2554+
rawEvent: {
2555+
inputType: 'insertText',
2556+
data: 'x',
2557+
} as any,
2558+
};
2559+
2560+
const result = plugin.willHandleEventExclusively(event);
2561+
expect(result).toBe(false);
2562+
});
2563+
2564+
it('should return false when input type is not insertText', () => {
2565+
mockSelection.range.startContainer.textContent = 'a.';
2566+
2567+
const event: EditorInputEvent = {
2568+
eventType: 'input',
2569+
rawEvent: {
2570+
inputType: 'deleteContentBackward',
2571+
data: ' ',
2572+
} as any,
2573+
};
2574+
2575+
const result = plugin.willHandleEventExclusively(event);
2576+
expect(result).toBe(false);
2577+
});
2578+
2579+
it('should return false when selection is not collapsed', () => {
2580+
mockSelection.range.collapsed = false;
2581+
mockSelection.range.startContainer.textContent = 'a.';
2582+
2583+
const event: EditorInputEvent = {
2584+
eventType: 'input',
2585+
rawEvent: {
2586+
inputType: 'insertText',
2587+
data: ' ',
2588+
} as any,
2589+
};
2590+
2591+
const result = plugin.willHandleEventExclusively(event);
2592+
expect(result).toBe(false);
2593+
});
2594+
2595+
it('should return false when there is no selection', () => {
2596+
mockEditor.getDOMSelection = () => null;
2597+
2598+
const event: EditorInputEvent = {
2599+
eventType: 'input',
2600+
rawEvent: {
2601+
inputType: 'insertText',
2602+
data: ' ',
2603+
} as any,
2604+
};
2605+
2606+
const result = plugin.willHandleEventExclusively(event);
2607+
expect(result).toBe(false);
2608+
});
2609+
2610+
it('should return false when selection type is not range', () => {
2611+
mockSelection.type = 'table';
2612+
2613+
const event: EditorInputEvent = {
2614+
eventType: 'input',
2615+
rawEvent: {
2616+
inputType: 'insertText',
2617+
data: ' ',
2618+
} as any,
2619+
};
2620+
2621+
const result = plugin.willHandleEventExclusively(event);
2622+
expect(result).toBe(false);
2623+
});
2624+
2625+
it('should return false when previous text is empty', () => {
2626+
mockSelection.range.startContainer.textContent = '';
2627+
2628+
const event: EditorInputEvent = {
2629+
eventType: 'input',
2630+
rawEvent: {
2631+
inputType: 'insertText',
2632+
data: ' ',
2633+
} as any,
2634+
};
2635+
2636+
const result = plugin.willHandleEventExclusively(event);
2637+
expect(result).toBe(false);
2638+
});
2639+
2640+
it('should return false when previous text is null', () => {
2641+
mockSelection.range.startContainer.textContent = null;
2642+
2643+
const event: EditorInputEvent = {
2644+
eventType: 'input',
2645+
rawEvent: {
2646+
inputType: 'insertText',
2647+
data: ' ',
2648+
} as any,
2649+
};
2650+
2651+
const result = plugin.willHandleEventExclusively(event);
2652+
expect(result).toBe(false);
2653+
});
2654+
2655+
it('should return false when previous text does not match patterns', () => {
2656+
mockSelection.range.startContainer.textContent = 'normal text';
2657+
2658+
const event: EditorInputEvent = {
2659+
eventType: 'input',
2660+
rawEvent: {
2661+
inputType: 'insertText',
2662+
data: ' ',
2663+
} as any,
2664+
};
2665+
2666+
const result = plugin.willHandleEventExclusively(event);
2667+
expect(result).toBe(false);
2668+
});
2669+
2670+
it('should return false for non-input events', () => {
2671+
const event: KeyDownEvent = {
2672+
eventType: 'keyDown',
2673+
rawEvent: {
2674+
key: 'Enter',
2675+
} as any,
2676+
};
2677+
2678+
const result = plugin.willHandleEventExclusively(event);
2679+
expect(result).toBe(false);
2680+
});
2681+
2682+
it('should return false when editor is not initialized', () => {
2683+
const uninitializedPlugin = new AutoFormatPlugin();
2684+
2685+
const event: EditorInputEvent = {
2686+
eventType: 'input',
2687+
rawEvent: {
2688+
inputType: 'insertText',
2689+
data: ' ',
2690+
} as any,
2691+
};
2692+
2693+
const result = uninitializedPlugin.willHandleEventExclusively(event);
2694+
expect(result).toBe(false);
2695+
});
2696+
2697+
it('should handle multiple valid patterns', () => {
2698+
const testCases = [
2699+
{ text: 'a)', expected: true },
2700+
{ text: 'B.', expected: true },
2701+
{ text: 'z-', expected: true },
2702+
{ text: 'Visit www.example.com', expected: true },
2703+
{ text: 'Check https://test.com', expected: true },
2704+
{ text: 'Email mailto:test@example.com', expected: true },
2705+
];
2706+
2707+
testCases.forEach(({ text, expected }) => {
2708+
mockSelection.range.startContainer.textContent = text;
2709+
2710+
const event: EditorInputEvent = {
2711+
eventType: 'input',
2712+
rawEvent: {
2713+
inputType: 'insertText',
2714+
data: ' ',
2715+
} as any,
2716+
};
2717+
2718+
const result = plugin.willHandleEventExclusively(event);
2719+
expect(result).toBe(expected);
2720+
});
2721+
});
2722+
});
24862723
});

0 commit comments

Comments
 (0)