commit
aaac4fbea7
|
@ -1,7 +1,8 @@
|
||||||
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
|
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
|
||||||
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true"
|
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true"
|
||||||
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)"
|
[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>
|
</div>
|
||||||
|
|
||||||
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
|
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
|
||||||
|
|
|
@ -108,6 +108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
protected resizeListener?: CoreEventObserver;
|
protected resizeListener?: CoreEventObserver;
|
||||||
protected domPromise?: CoreCancellablePromise<void>;
|
protected domPromise?: CoreCancellablePromise<void>;
|
||||||
protected buttonsDomPromise?: CoreCancellablePromise<void>;
|
protected buttonsDomPromise?: CoreCancellablePromise<void>;
|
||||||
|
protected shortcutCommands?: Record<string, EditorCommand>;
|
||||||
|
|
||||||
rteEnabled = false;
|
rteEnabled = false;
|
||||||
isPhone = false;
|
isPhone = false;
|
||||||
|
@ -171,11 +172,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
this.setContent(this.control?.value);
|
this.setContent(this.control?.value);
|
||||||
this.originalContent = this.control?.value;
|
this.originalContent = this.control?.value;
|
||||||
this.lastDraft = 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();
|
|
||||||
this.editorElement.onkeydown = event => this.moveCursor(event);
|
|
||||||
|
|
||||||
// Use paragraph on enter.
|
// Use paragraph on enter.
|
||||||
document.execCommand('DefaultParagraphSeparator', false, 'p');
|
document.execCommand('DefaultParagraphSeparator', false, 'p');
|
||||||
|
@ -274,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.
|
* Resize editor to maximize the space occupied.
|
||||||
*/
|
*/
|
||||||
|
@ -374,64 +390,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
this.contentChanged.emit(this.control?.value);
|
this.contentChanged.emit(this.control?.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On key down function to move the cursor.
|
|
||||||
* https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div
|
|
||||||
*
|
|
||||||
* @param event The event.
|
|
||||||
*/
|
|
||||||
moveCursor(event: KeyboardEvent): void {
|
|
||||||
if (!this.rteEnabled || !this.editorElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key != 'ArrowLeft' && event.key != 'ArrowRight') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stopBubble(event);
|
|
||||||
|
|
||||||
const move = event.key === 'ArrowLeft' ? -1 : +1;
|
|
||||||
const cursor = this.getCurrentCursorPosition(this.editorElement);
|
|
||||||
|
|
||||||
this.setCurrentCursorPosition(this.editorElement, cursor + move);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of chars from the beggining where is placed the cursor.
|
|
||||||
*
|
|
||||||
* @param parent Parent where to get the position from.
|
|
||||||
* @returns Position in chars.
|
|
||||||
*/
|
|
||||||
protected getCurrentCursorPosition(parent: Node): number {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
|
|
||||||
let charCount = -1;
|
|
||||||
|
|
||||||
if (selection?.focusNode && parent.contains(selection.focusNode)) {
|
|
||||||
let node: Node | null = selection.focusNode;
|
|
||||||
charCount = selection.focusOffset;
|
|
||||||
|
|
||||||
while (node) {
|
|
||||||
if (node.isSameNode(parent)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.previousSibling) {
|
|
||||||
node = node.previousSibling;
|
|
||||||
charCount += (node.textContent || '').length;
|
|
||||||
} else {
|
|
||||||
node = node.parentNode;
|
|
||||||
if (node === null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return charCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the caret position on the character number.
|
* Set the caret position on the character number.
|
||||||
*
|
*
|
||||||
|
@ -446,6 +404,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
* @param node Node where to start.
|
* @param node Node where to start.
|
||||||
* @param range Previous calculated range.
|
* @param range Previous calculated range.
|
||||||
* @param chars Object with counting of characters (input-output param).
|
* @param chars Object with counting of characters (input-output param).
|
||||||
|
* @param chars.count Count of characters.
|
||||||
* @returns Selection range.
|
* @returns Selection range.
|
||||||
*/
|
*/
|
||||||
const setRange = (node: Node, range: Range, chars: { count: number }): Range => {
|
const setRange = (node: Node, range: Range, chars: { count: number }): Range => {
|
||||||
|
@ -641,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
|
* API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
|
||||||
*
|
*
|
||||||
* @param event Event data
|
* @param event Event data
|
||||||
|
@ -660,6 +619,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
return;
|
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') {
|
if (parameters == 'block') {
|
||||||
document.execCommand('formatBlock', false, '<' + command + '>');
|
document.execCommand('formatBlock', false, '<' + command + '>');
|
||||||
|
|
||||||
|
@ -1061,6 +1032,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async scanQR(event: Event): Promise<void> {
|
async scanQR(event: Event): Promise<void> {
|
||||||
|
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.stopBubble(event);
|
this.stopBubble(event);
|
||||||
|
|
||||||
// Scan for a QR code.
|
// Scan for a QR code.
|
||||||
|
@ -1117,4 +1092,121 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
||||||
this.buttonsDomPromise?.cancel();
|
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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue