Skip to content

Commit 40ebf51

Browse files
[FHL] Add InsertHorizontalLine to AutoFormat functionality (#2958)
* Add horizontal line insertion functionality and update options * Enhance horizontal line insertion by enabling undo with backspace * Disable automatic horizontal line insertion in auto format options * Refactor * Add setEditorStyle spy to SelectionPlugin tests * Refactor * Update horizontal line styles for consistency and improved appearance * Update horizontal line styles in tests for consistency * Revert unneeded change --------- Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com>
1 parent fd1c92f commit 40ebf51

File tree

7 files changed

+655
-0
lines changed

7 files changed

+655
-0
lines changed

demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const initialState: OptionState = {
4848
autoMailto: true,
4949
autoTel: true,
5050
removeListMargins: false,
51+
autoHorizontalLine: true,
5152
},
5253
markdownOptions: {
5354
bold: true,

demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
113113
private autoTel = React.createRef<HTMLInputElement>();
114114
private autoMailto = React.createRef<HTMLInputElement>();
115115
private removeListMargins = React.createRef<HTMLInputElement>();
116+
private horizontalLine = React.createRef<HTMLInputElement>();
116117
private markdownBold = React.createRef<HTMLInputElement>();
117118
private markdownItalic = React.createRef<HTMLInputElement>();
118119
private markdownStrikethrough = React.createRef<HTMLInputElement>();
@@ -188,6 +189,13 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
188189
(state, value) =>
189190
(state.autoFormatOptions.removeListMargins = value)
190191
)}
192+
{this.renderCheckBox(
193+
'Horizontal Line',
194+
this.horizontalLine,
195+
this.props.state.autoFormatOptions.autoHorizontalLine,
196+
(state, value) =>
197+
(state.autoFormatOptions.autoHorizontalLine = value)
198+
)}
191199
</>
192200
)}
193201
{this.renderPluginItem(

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChangeSource } from 'roosterjs-content-model-dom';
2+
import { checkAndInsertHorizontalLine } from './horizontalLine/checkAndInsertHorizontalLine';
23
import { createLink } from './link/createLink';
34
import { formatTextSegmentBeforeSelectionMarker, promoteLink } from 'roosterjs-content-model-api';
45
import { keyboardListTrigger } from './list/keyboardListTrigger';
@@ -52,6 +53,7 @@ const DefaultOptions: Partial<AutoFormatOptions> = {
5253
autoFraction: false,
5354
autoOrdinals: false,
5455
removeListMargins: false,
56+
autoHorizontalLine: false,
5557
};
5658

5759
/**
@@ -72,6 +74,7 @@ export class AutoFormatPlugin implements EditorPlugin {
7274
* - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false.
7375
* - autoTel: A boolean that enables or disables automatic hyperlink telephone numbers transformation. Defaults to false.
7476
* - autoMailto: A boolean that enables or disables automatic hyperlink email address transformation. Defaults to false.
77+
* - autoHorizontalLine: A boolean that enables or disables automatic horizontal line creation. Defaults to false.
7578
*/
7679
constructor(private options: AutoFormatOptions = DefaultOptions) {}
7780

@@ -270,10 +273,20 @@ export class AutoFormatPlugin implements EditorPlugin {
270273
}
271274
);
272275
}
276+
break;
277+
case 'Enter':
278+
this.handleEnterKey(editor, event);
279+
break;
273280
}
274281
}
275282
}
276283

284+
private handleEnterKey(editor: IEditor, event: KeyDownEvent) {
285+
if (this.options.autoHorizontalLine) {
286+
checkAndInsertHorizontalLine(editor, event);
287+
}
288+
}
289+
277290
private handleContentChangedEvent(editor: IEditor, event: ContentChangedEvent) {
278291
const { autoLink, autoTel, autoMailto } = this.options;
279292
if (event.source == 'Paste' && (autoLink || autoTel || autoMailto)) {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { formatTextSegmentBeforeSelectionMarker } from 'roosterjs-content-model-api';
2+
import type {
3+
ContentModelDividerFormat,
4+
FormatContentModelContext,
5+
IEditor,
6+
KeyDownEvent,
7+
ShallowMutableContentModelDocument,
8+
} from 'roosterjs-content-model-types';
9+
import {
10+
addBlock,
11+
ChangeSource,
12+
createContentModelDocument,
13+
createDivider,
14+
mergeModel,
15+
} from 'roosterjs-content-model-dom';
16+
17+
/**
18+
* @internal
19+
*/
20+
export type HorizontalLineTriggerCharacter = '-' | '=' | '_' | '*' | '~' | '#';
21+
const HorizontalLineTriggerCharacters: HorizontalLineTriggerCharacter[] = [
22+
'-',
23+
'=',
24+
'_',
25+
'*',
26+
'~',
27+
'#',
28+
];
29+
30+
const commonStyles: ContentModelDividerFormat = {
31+
width: '98%',
32+
display: 'inline-block',
33+
};
34+
35+
const HorizontalLineStyles: Map<
36+
HorizontalLineTriggerCharacter,
37+
ContentModelDividerFormat
38+
> = new Map([
39+
[
40+
'-',
41+
{
42+
borderTop: '1px none',
43+
borderRight: '1px none',
44+
borderBottom: '1px solid',
45+
borderLeft: '1px none',
46+
...commonStyles,
47+
},
48+
],
49+
[
50+
'=',
51+
{
52+
borderTop: '3pt double',
53+
borderRight: '3pt none',
54+
borderBottom: '3pt none',
55+
borderLeft: '3pt none',
56+
...commonStyles,
57+
},
58+
],
59+
[
60+
'_',
61+
{
62+
borderTop: '1px solid',
63+
borderRight: '1px none',
64+
borderBottom: '1px solid',
65+
borderLeft: '1px none',
66+
...commonStyles,
67+
},
68+
],
69+
[
70+
'*',
71+
{
72+
borderTop: '1px none',
73+
borderRight: '1px none',
74+
borderBottom: '3px dotted',
75+
borderLeft: '1px none',
76+
...commonStyles,
77+
},
78+
],
79+
[
80+
'~',
81+
{
82+
borderTop: '1px none',
83+
borderRight: '1px none',
84+
borderBottom: '1px solid',
85+
borderLeft: '1px none',
86+
...commonStyles,
87+
},
88+
],
89+
[
90+
'#',
91+
{
92+
borderTop: '3pt double',
93+
borderRight: '3pt none',
94+
borderBottom: '3pt double',
95+
borderLeft: '3pt none',
96+
...commonStyles,
97+
},
98+
],
99+
]);
100+
101+
/**
102+
* @internal exported only for unit test
103+
*
104+
* Create a horizontal line and insert it into the model
105+
*
106+
* @param model the model to insert horizontal line into
107+
* @param context the formatting context
108+
*/
109+
export function insertHorizontalLineIntoModel(
110+
model: ShallowMutableContentModelDocument,
111+
context: FormatContentModelContext,
112+
triggerChar: HorizontalLineTriggerCharacter
113+
) {
114+
const hr = createDivider('hr', HorizontalLineStyles.get(triggerChar));
115+
const doc = createContentModelDocument();
116+
addBlock(doc, hr);
117+
118+
mergeModel(model, doc, context);
119+
}
120+
121+
/**
122+
* @internal
123+
*
124+
* Check if the current line should be formatted as horizontal line, and insert horizontal line if needed
125+
*
126+
* @param editor The editor to check and insert horizontal line
127+
* @param event The keydown event
128+
* @returns True if horizontal line is inserted, otherwise false
129+
*/
130+
export function checkAndInsertHorizontalLine(editor: IEditor, event: KeyDownEvent) {
131+
return formatTextSegmentBeforeSelectionMarker(
132+
editor,
133+
(model, _, para, __, context) => {
134+
const allText = para.segments.reduce(
135+
(acc, segment) => (segment.segmentType === 'Text' ? acc + segment.text : acc),
136+
''
137+
);
138+
// At least 3 characters are needed to trigger horizontal line
139+
if (allText.length < 3) {
140+
return false;
141+
}
142+
143+
return HorizontalLineTriggerCharacters.some(triggerCharacter => {
144+
const shouldFormat = allText.split('').every(char => char === triggerCharacter);
145+
if (shouldFormat) {
146+
para.segments = para.segments.filter(s => s.segmentType != 'Text');
147+
insertHorizontalLineIntoModel(model, context, triggerCharacter);
148+
event.rawEvent.preventDefault();
149+
context.canUndoByBackspace = true;
150+
}
151+
return shouldFormat;
152+
});
153+
},
154+
{
155+
changeSource: ChangeSource.AutoFormat,
156+
apiName: 'autoHorizontalLine',
157+
}
158+
);
159+
}

packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ export interface AutoFormatOptions extends AutoLinkOptions {
3333
* Remove the margins of auto triggered list
3434
*/
3535
removeListMargins?: boolean;
36+
37+
/**
38+
* Auto Horizontal line
39+
*/
40+
autoHorizontalLine?: boolean;
3641
}

0 commit comments

Comments
 (0)