MOBILE-3748 rte: Implement keyboard shortcuts

main
Noel De Martin 2023-05-08 13:17:30 +02:00
parent dee6c915f8
commit bf7659223d
2 changed files with 153 additions and 6 deletions

View File

@ -1,7 +1,8 @@
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true"
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)"
(blur)="hideToolbar($event)">
(blur)="hideToolbar($event)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()"
(input)="onChange()">
</div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"

View File

@ -108,6 +108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
protected resizeListener?: CoreEventObserver;
protected domPromise?: CoreCancellablePromise<void>;
protected buttonsDomPromise?: CoreCancellablePromise<void>;
protected shortcutCommands?: Record<string, EditorCommand>;
rteEnabled = false;
isPhone = false;
@ -171,10 +172,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.setContent(this.control?.value);
this.originalContent = this.control?.value;
this.lastDraft = this.control?.value;
this.editorElement.onchange = () => this.onChange();
this.editorElement.onkeyup = () => this.onChange();
this.editorElement.onpaste = () => this.onChange();
this.editorElement.oninput = () => this.onChange();
// Use paragraph on enter.
document.execCommand('DefaultParagraphSeparator', false, 'p');
@ -273,6 +270,26 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
});
}
/**
* Handle keydown events in the editor.
*
* @param event Event
*/
onKeyDown(event: KeyboardEvent): void {
this.onChange();
const shortcutId = this.getShortcutId(event);
const commands = this.getShortcutCommands();
const command = commands[shortcutId];
if (!command) {
return;
}
this.stopBubble(event);
this.executeCommand(command);
}
/**
* Resize editor to maximize the space occupied.
*/
@ -583,7 +600,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
}
/**
* Execute an action over the selected text.
* Execute an action over the selected text when a button is activated.
* API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
*
* @param event Event data
@ -602,6 +619,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
return;
}
this.executeCommand({ name: command, parameters });
}
/**
* Execute an action over the selected text.
* API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
*
* @param command Editor command.
* @param command.name Command name.
* @param command.parameters Command parameters.
*/
protected executeCommand({ name: command, parameters }: EditorCommand): void {
if (parameters == 'block') {
document.execCommand('formatBlock', false, '<' + command + '>');
@ -1059,4 +1088,121 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.buttonsDomPromise?.cancel();
}
/**
* Get commands triggered by keyboard shortcuts.
*
* @returns Commands dictionary indexed by their corresponding keyboard shortcut id.
*/
getShortcutCommands(): Record<string, EditorCommand> {
if (!this.shortcutCommands) {
const isIOS = CorePlatform.isIOS();
const metaKey = isIOS ? 'metaKey' : 'ctrlKey';
const shiftKey = isIOS ? 'ctrlKey' : 'shiftKey';
// Same shortcuts as TinyMCE:
// @see https://www.tiny.cloud/docs/advanced/keyboard-shortcuts/
const shortcuts: { code: string; modifiers: (keyof KeyboardShortcut)[]; command: EditorCommand }[] = [
{
code: 'KeyB',
modifiers: [metaKey],
command: {
name: 'bold',
parameters: 'strong',
},
},
{
code: 'KeyI',
modifiers: [metaKey],
command: {
name: 'italic',
parameters: 'em',
},
},
{
code: 'KeyU',
modifiers: [metaKey],
command: {
name: 'underline',
parameters: 'u',
},
},
{
code: 'Digit3',
modifiers: ['altKey', shiftKey],
command: {
name: 'h3',
parameters: 'block',
},
},
{
code: 'Digit4',
modifiers: ['altKey', shiftKey],
command: {
name: 'h4',
parameters: 'block',
},
},
{
code: 'Digit5',
modifiers: ['altKey', shiftKey],
command: {
name: 'h5',
parameters: 'block',
},
},
{
code: 'Digit7',
modifiers: ['altKey', shiftKey],
command: {
name: 'p',
parameters: 'block',
},
},
];
this.shortcutCommands = shortcuts.reduce((shortcuts, { code, modifiers, command }) => {
const id = this.getShortcutId({
code: code,
altKey: modifiers.includes('altKey'),
metaKey: modifiers.includes('metaKey'),
shiftKey: modifiers.includes('shiftKey'),
ctrlKey: modifiers.includes('ctrlKey'),
});
shortcuts[id] = command;
return shortcuts;
}, {} as Record<string, EditorCommand>);
}
return this.shortcutCommands;
}
/**
* Get a unique identifier for a given keyboard shortcut.
*
* @param shortcut Shortcut.
* @returns Identifier.
*/
protected getShortcutId(shortcut: KeyboardShortcut): string {
return (shortcut.altKey ? '1' : '0')
+ (shortcut.metaKey ? '1' : '0')
+ (shortcut.shiftKey ? '1' : '0')
+ (shortcut.ctrlKey ? '1' : '0')
+ shortcut.code;
}
}
/**
* Combination
*/
type KeyboardShortcut = Pick<KeyboardEvent, 'code' | 'altKey' | 'metaKey' | 'ctrlKey' | 'shiftKey'>;
/**
* Editor command.
*/
interface EditorCommand {
name: string;
parameters?: string;
}