MOBILE-4565 rte: Fix rte focus and blur problems
This commit is contained in:
		
							parent
							
								
									64ce8c78f6
								
							
						
					
					
						commit
						f9ddfb48c9
					
				
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB  | 
@ -1,8 +1,7 @@
 | 
			
		||||
<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)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()"
 | 
			
		||||
        (input)="onChange()">
 | 
			
		||||
        (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,18 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    background: var(--background);
 | 
			
		||||
    border: 1px solid var(--stroke);
 | 
			
		||||
    border-radius: var(--mdl-shape-borderRadius-md);
 | 
			
		||||
 | 
			
		||||
    &:focus-within {
 | 
			
		||||
        border-color: var(--a11y-focus-color);
 | 
			
		||||
        border-width: 2px;
 | 
			
		||||
        outline: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-rte-editor, .core-textarea, textarea {
 | 
			
		||||
        outline: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-rte-editor-container {
 | 
			
		||||
        max-height: calc(100% - 46px);
 | 
			
		||||
@ -105,7 +117,6 @@
 | 
			
		||||
        flex-grow: 0;
 | 
			
		||||
        flex-shrink: 0;
 | 
			
		||||
        background-color: var(--toobar-background);
 | 
			
		||||
        padding-top: 5px;
 | 
			
		||||
        border-top: 1px solid var(--stroke);
 | 
			
		||||
 | 
			
		||||
        swiper-container {
 | 
			
		||||
@ -122,7 +133,7 @@
 | 
			
		||||
            height: 36px;
 | 
			
		||||
            padding-right: 6px;
 | 
			
		||||
            padding-left: 6px;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
            margin: 2px auto;
 | 
			
		||||
            font-size: 18px;
 | 
			
		||||
            background-color: var(--toobar-background);
 | 
			
		||||
            border-radius: var(--mdl-shape-borderRadius-xs);
 | 
			
		||||
 | 
			
		||||
@ -115,6 +115,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    protected keyboardObserver?: CoreEventObserver;
 | 
			
		||||
    protected resetObserver?: CoreEventObserver;
 | 
			
		||||
    protected labelObserver?: MutationObserver;
 | 
			
		||||
    protected contentObserver?: MutationObserver;
 | 
			
		||||
    protected initHeightInterval?: number;
 | 
			
		||||
    protected isCurrentView = true;
 | 
			
		||||
    protected toolbarButtonWidth = 44;
 | 
			
		||||
@ -126,7 +127,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    protected draftWasRestored = false;
 | 
			
		||||
    protected originalContent?: string;
 | 
			
		||||
    protected resizeFunction?: () => Promise<number>;
 | 
			
		||||
    protected selectionChangeFunction?: () => void;
 | 
			
		||||
    protected selectionChangeFunction = (): void => this.updateToolbarStyles();
 | 
			
		||||
    protected languageChangedSubscription?: Subscription;
 | 
			
		||||
    protected resizeListener?: CoreEventObserver;
 | 
			
		||||
    protected domPromise?: CoreCancellablePromise<void>;
 | 
			
		||||
@ -226,6 +227,15 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
        }
 | 
			
		||||
        ionItem.classList.add('item-rte');
 | 
			
		||||
 | 
			
		||||
        if (this.editorElement) {
 | 
			
		||||
            const debounceMutation = CoreUtils.debounce(() => {
 | 
			
		||||
                this.onChange();
 | 
			
		||||
            }, 20);
 | 
			
		||||
 | 
			
		||||
            this.contentObserver = new MutationObserver(debounceMutation);
 | 
			
		||||
            this.contentObserver.observe(this.editorElement, { childList: true, subtree: true, characterData: true });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const label = ionItem.querySelector('ion-label');
 | 
			
		||||
 | 
			
		||||
        if (!label) {
 | 
			
		||||
@ -253,7 +263,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    protected setListeners(): void {
 | 
			
		||||
        // Listen for changes on the control to update the editor (if it is updated from outside of this component).
 | 
			
		||||
        this.valueChangeSubscription = this.control?.valueChanges.subscribe((newValue) => {
 | 
			
		||||
            if (this.draftWasRestored && this.originalContent == newValue) {
 | 
			
		||||
            if (this.draftWasRestored && this.originalContent === newValue) {
 | 
			
		||||
                // A draft was restored and the content hasn't changed in the site. Use the draft value instead of this one.
 | 
			
		||||
                this.control?.setValue(this.lastDraft, { emitEvent: false });
 | 
			
		||||
 | 
			
		||||
@ -282,7 +292,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
            this.windowResized();
 | 
			
		||||
        }, 50);
 | 
			
		||||
 | 
			
		||||
        document.addEventListener('selectionchange', this.selectionChangeFunction = () => this.updateToolbarStyles());
 | 
			
		||||
        document.addEventListener('selectionchange', this.selectionChangeFunction);
 | 
			
		||||
 | 
			
		||||
        this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => {
 | 
			
		||||
            // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon.
 | 
			
		||||
@ -304,8 +314,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param event Event
 | 
			
		||||
     */
 | 
			
		||||
    onKeyDown(event: KeyboardEvent): void {
 | 
			
		||||
        this.onChange();
 | 
			
		||||
 | 
			
		||||
        const shortcutId = this.getShortcutId(event);
 | 
			
		||||
        const commands = this.getShortcutCommands();
 | 
			
		||||
        const command = commands[shortcutId];
 | 
			
		||||
@ -364,7 +372,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
 | 
			
		||||
        // Get first children with content, not fixed.
 | 
			
		||||
        let scrollContentHeight = 0;
 | 
			
		||||
        while (scrollContentHeight == 0 && content?.children) {
 | 
			
		||||
        while (scrollContentHeight === 0 && content?.children) {
 | 
			
		||||
            const children = Array.from(content.children)
 | 
			
		||||
                .filter((element) => element.slot !== 'fixed' && !element.classList.contains('core-loading-container'));
 | 
			
		||||
 | 
			
		||||
@ -489,7 +497,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param event The event.
 | 
			
		||||
     */
 | 
			
		||||
    async toggleEditor(event: Event): Promise<void> {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -581,7 +589,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @returns If value is null only a white space.
 | 
			
		||||
     */
 | 
			
		||||
    protected isNullOrWhiteSpace(value: string | null | undefined): boolean {
 | 
			
		||||
        if (value == null || value === undefined) {
 | 
			
		||||
        if (value === null || value === undefined) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -602,10 +610,17 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isNullOrWhiteSpace(value)) {
 | 
			
		||||
            this.editorElement.innerHTML = '<p></p>';
 | 
			
		||||
            // Avoid loops.
 | 
			
		||||
            if (this.editorElement.innerHTML !== '<p></p>') {
 | 
			
		||||
                this.editorElement.innerHTML = '<p></p>';
 | 
			
		||||
            }
 | 
			
		||||
            this.textarea.value = '';
 | 
			
		||||
        } else {
 | 
			
		||||
            this.editorElement.innerHTML = value || '';
 | 
			
		||||
            value = value || '';
 | 
			
		||||
            // Avoid loops.
 | 
			
		||||
            if (this.editorElement.innerHTML !== value) {
 | 
			
		||||
                this.editorElement.innerHTML = value;
 | 
			
		||||
            }
 | 
			
		||||
            this.textarea.value = value;
 | 
			
		||||
            this.treatExternalContent();
 | 
			
		||||
        }
 | 
			
		||||
@ -637,7 +652,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     *                      toolbar styles button when set.
 | 
			
		||||
     */
 | 
			
		||||
    buttonAction(event: Event, command: string, parameters?: string): void {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -659,7 +674,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param command.parameters Command parameters.
 | 
			
		||||
     */
 | 
			
		||||
    protected executeCommand({ name: command, parameters }: EditorCommand): void {
 | 
			
		||||
        if (parameters == 'block') {
 | 
			
		||||
        if (parameters === 'block') {
 | 
			
		||||
            // eslint-disable-next-line deprecation/deprecation
 | 
			
		||||
            document.execCommand('formatBlock', false, '<' + command + '>');
 | 
			
		||||
 | 
			
		||||
@ -676,7 +691,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
        // Modern browsers are using non a11y tags, so replace them.
 | 
			
		||||
        if (command === 'bold') {
 | 
			
		||||
            this.replaceTags(['b'], ['strong']);
 | 
			
		||||
        } else if (command == 'italic') {
 | 
			
		||||
        } else if (command === 'italic') {
 | 
			
		||||
            this.replaceTags(['i'], ['em']);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -715,14 +730,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     * @param force If true it will not check the target of the event.
 | 
			
		||||
     */
 | 
			
		||||
    hideToolbar(event: Event, force = false): void {
 | 
			
		||||
    hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, force = false): void {
 | 
			
		||||
        if (!force && event.target && this.element.contains(event.target as HTMLElement)) {
 | 
			
		||||
            // Do not hide if clicked inside the editor area, except forced.
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -748,7 +763,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the toolbar.
 | 
			
		||||
     */
 | 
			
		||||
    showToolbar(event: Event): void {
 | 
			
		||||
    showToolbar(event: FocusEvent): void {
 | 
			
		||||
        this.updateToolbarButtons();
 | 
			
		||||
 | 
			
		||||
        this.element.classList.add('ion-touched');
 | 
			
		||||
@ -779,14 +794,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    downAction(event: Event): void {
 | 
			
		||||
        if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const selection = window.getSelection()?.toString();
 | 
			
		||||
 | 
			
		||||
        // When RTE is focused with a whole paragraph in desktop the stopBubble will not fire click.
 | 
			
		||||
        if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection == '') {
 | 
			
		||||
        if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection === '') {
 | 
			
		||||
            this.stopBubble(event);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -795,7 +810,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * Method that shows the next toolbar buttons.
 | 
			
		||||
     */
 | 
			
		||||
    async toolbarNext(event: Event): Promise<void> {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * Method that shows the previous toolbar buttons.
 | 
			
		||||
     */
 | 
			
		||||
    async toolbarPrev(event: Event): Promise<void> {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * Update the number of toolbar buttons displayed.
 | 
			
		||||
     */
 | 
			
		||||
    async updateToolbarButtons(): Promise<void> {
 | 
			
		||||
        if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent == null) {
 | 
			
		||||
        if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) {
 | 
			
		||||
            // Don't calculate if component isn't in current view, the calculations are wrong.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -879,15 +894,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     */
 | 
			
		||||
    updateToolbarStyles(): void {
 | 
			
		||||
        const node = window.getSelection()?.focusNode;
 | 
			
		||||
        if (!node) {
 | 
			
		||||
 | 
			
		||||
        if (!node || !this.element.contains(node)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement;
 | 
			
		||||
        let element = node.nodeType === 1 ? node as HTMLElement : node.parentElement;
 | 
			
		||||
 | 
			
		||||
        const styles = {};
 | 
			
		||||
 | 
			
		||||
        while (element != null && element !== this.editorElement) {
 | 
			
		||||
        while (element !== null && element !== this.editorElement) {
 | 
			
		||||
            const tagName = element.tagName.toLowerCase();
 | 
			
		||||
 | 
			
		||||
            if (this.toolbarStyles[tagName]) {
 | 
			
		||||
                styles[tagName] = 'true';
 | 
			
		||||
            }
 | 
			
		||||
@ -906,7 +924,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if should auto save drafts.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns {boolean} Whether it should auto save drafts.
 | 
			
		||||
     * @returns Whether it should auto save drafts.
 | 
			
		||||
     */
 | 
			
		||||
    protected shouldAutoSaveDrafts(): boolean {
 | 
			
		||||
        return !!CoreSites.getCurrentSite() &&
 | 
			
		||||
@ -943,8 +961,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
            let draftText = entry.drafttext || '';
 | 
			
		||||
 | 
			
		||||
            // Revert untouched editor contents to an empty string.
 | 
			
		||||
            if (draftText == '<p></p>' || draftText == '<p><br></p>' || draftText == '<br>' ||
 | 
			
		||||
                    draftText == '<p> </p>' || draftText == '<p><br> </p>') {
 | 
			
		||||
            if (draftText === '<p></p>' || draftText === '<p><br></p>' || draftText === '<br>' ||
 | 
			
		||||
                    draftText === '<p> </p>' || draftText === '<p><br> </p>') {
 | 
			
		||||
                draftText = '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -977,7 +995,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
 | 
			
		||||
            const newText = this.control.value ?? '';
 | 
			
		||||
 | 
			
		||||
            if (this.lastDraft == newText) {
 | 
			
		||||
            if (this.lastDraft === newText) {
 | 
			
		||||
                // Text hasn't changed, nothing to save.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@ -1009,7 +1027,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
        this.resetObserver = CoreEvents.on(CoreEvents.FORM_ACTION, async (data: CoreEventFormActionData) => {
 | 
			
		||||
            const form = this.element.closest('form');
 | 
			
		||||
 | 
			
		||||
            if (data.form && form && data.form == form) {
 | 
			
		||||
            if (data.form && form && data.form === form) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await CoreEditorOffline.deleteDraft(
 | 
			
		||||
                        this.contextLevel || ContextLevel.SYSTEM,
 | 
			
		||||
@ -1048,7 +1066,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async scanQR(event: Event): Promise<void> {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1097,14 +1115,20 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.valueChangeSubscription?.unsubscribe();
 | 
			
		||||
        this.languageChangedSubscription?.unsubscribe();
 | 
			
		||||
        this.selectionChangeFunction && document.removeEventListener('selectionchange', this.selectionChangeFunction);
 | 
			
		||||
 | 
			
		||||
        document.removeEventListener('selectionchange', this.selectionChangeFunction);
 | 
			
		||||
 | 
			
		||||
        clearInterval(this.initHeightInterval);
 | 
			
		||||
        clearInterval(this.autoSaveInterval);
 | 
			
		||||
        clearTimeout(this.hideMessageTimeout);
 | 
			
		||||
 | 
			
		||||
        this.resetObserver?.off();
 | 
			
		||||
        this.keyboardObserver?.off();
 | 
			
		||||
        this.labelObserver?.disconnect();
 | 
			
		||||
        this.resizeListener?.off();
 | 
			
		||||
 | 
			
		||||
        this.labelObserver?.disconnect();
 | 
			
		||||
        this.contentObserver?.disconnect();
 | 
			
		||||
 | 
			
		||||
        this.domPromise?.cancel();
 | 
			
		||||
        this.buttonsDomPromise?.cancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -235,7 +235,7 @@ export class CoreLoginSitePage implements OnInit {
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate Url.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns {ValidatorFn} Validation results.
 | 
			
		||||
     * @returns Validation results.
 | 
			
		||||
     */
 | 
			
		||||
    protected moodleUrlValidator(): ValidatorFn {
 | 
			
		||||
        return (control: AbstractControl): ValidationErrors | null => {
 | 
			
		||||
 | 
			
		||||
@ -1129,7 +1129,7 @@ td {
 | 
			
		||||
    @include core-focus-background();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:not(.hydrated):not(.native-input) { // Not an ionic component.
 | 
			
		||||
:not(.hydrated):not(.native-input):not(.native-textarea) { // Not an ionic component.
 | 
			
		||||
    @include core-focus-outline();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user