MOBILE-2314 core: Implement custom modals
parent
f5fa9d12dc
commit
ba723dd899
|
@ -0,0 +1,45 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { ElementRef } from '@angular/core';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to build modals.
|
||||||
|
*/
|
||||||
|
export class CoreModalComponent<T=unknown> {
|
||||||
|
|
||||||
|
result: CorePromisedValue<T> = new CorePromisedValue();
|
||||||
|
|
||||||
|
constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
|
||||||
|
CoreDirectivesRegistry.register(element, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*
|
||||||
|
* @param result Result data, or error instance if the modal was closed with a failure.
|
||||||
|
*/
|
||||||
|
async close(result: T | Error): Promise<void> {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
this.result.reject(result);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.result.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-
|
||||||
import { CoreMessageComponent } from './message/message';
|
import { CoreMessageComponent } from './message/message';
|
||||||
import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
||||||
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
||||||
|
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -110,6 +111,7 @@ import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-
|
||||||
CoreHorizontalScrollControlsComponent,
|
CoreHorizontalScrollControlsComponent,
|
||||||
CoreSwipeNavigationTourComponent,
|
CoreSwipeNavigationTourComponent,
|
||||||
CoreRefreshButtonModalComponent,
|
CoreRefreshButtonModalComponent,
|
||||||
|
CoreSheetModalComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -163,6 +165,7 @@ import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-
|
||||||
CoreHorizontalScrollControlsComponent,
|
CoreHorizontalScrollControlsComponent,
|
||||||
CoreSwipeNavigationTourComponent,
|
CoreSwipeNavigationTourComponent,
|
||||||
CoreRefreshButtonModalComponent,
|
CoreRefreshButtonModalComponent,
|
||||||
|
CoreSheetModalComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<ion-backdrop></ion-backdrop>
|
||||||
|
<div class="sheet-modal--wrapper" #wrapper></div>
|
|
@ -0,0 +1,40 @@
|
||||||
|
@import "~theme/globals";
|
||||||
|
|
||||||
|
:host {
|
||||||
|
--backdrop-opacity: var(--ion-backdrop-opacity, 0.4);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
ion-backdrop {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-modal--wrapper {
|
||||||
|
border-radius: var(--big-radius) var(--big-radius) 0 0;
|
||||||
|
@include padding(24px, 16px, 24px, 16px);
|
||||||
|
|
||||||
|
background-color: var(--ion-overlay-background-color, var(--ion-background-color, #fff));
|
||||||
|
z-index: 3; // ion-backdrop has z-index 2
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
|
||||||
|
ion-backdrop {
|
||||||
|
opacity: var(--backdrop-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-modal--wrapper {
|
||||||
|
transform: translateY(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Constructor } from '@/core/utils/types';
|
||||||
|
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||||
|
import { CoreModalComponent } from '@classes/modal-component';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreModals } from '@services/modals';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AngularFrameworkDelegate } from '@singletons';
|
||||||
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'core-sheet-modal',
|
||||||
|
templateUrl: 'sheet-modal.html',
|
||||||
|
styleUrls: ['sheet-modal.scss'],
|
||||||
|
})
|
||||||
|
export class CoreSheetModalComponent<T extends CoreModalComponent> implements AfterViewInit {
|
||||||
|
|
||||||
|
@Input() component!: Constructor<T>;
|
||||||
|
@Input() componentProps?: Record<string, unknown>;
|
||||||
|
@ViewChild('wrapper') wrapper?: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
|
private element: HTMLElement;
|
||||||
|
private wrapperElement = new CorePromisedValue<HTMLElement>();
|
||||||
|
|
||||||
|
constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
CoreDirectivesRegistry.register(element, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (!this.wrapper) {
|
||||||
|
this.wrapperElement.reject(new Error('CoreSheetModalComponent wasn\'t mounted properly'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wrapperElement.resolve(this.wrapper.nativeElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show modal.
|
||||||
|
*
|
||||||
|
* @returns Component instance.
|
||||||
|
*/
|
||||||
|
async show(): Promise<T> {
|
||||||
|
const wrapper = await this.wrapperElement;
|
||||||
|
const element = await AngularFrameworkDelegate.attachViewToDom(wrapper, this.component, this.componentProps ?? {});
|
||||||
|
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
this.element.classList.add('active');
|
||||||
|
this.element.style.zIndex = `${20000 + CoreModals.getTopOverlayIndex()}`;
|
||||||
|
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
await CoreUtils.wait(300);
|
||||||
|
|
||||||
|
const instance = CoreDirectivesRegistry.resolve(element, this.component);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error('Modal not mounted properly');
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide modal.
|
||||||
|
*/
|
||||||
|
async hide(): Promise<void> {
|
||||||
|
this.element.classList.remove('active');
|
||||||
|
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
await CoreUtils.wait(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Constructor } from '@/core/utils/types';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreModalComponent } from '@classes/modal-component';
|
||||||
|
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||||
|
import { AngularFrameworkDelegate, makeSingleton } from '@singletons';
|
||||||
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles application modals.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreModalsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get index of the overlay on top of the stack.
|
||||||
|
*
|
||||||
|
* @returns Z-index of the overlay on top.
|
||||||
|
*/
|
||||||
|
getTopOverlayIndex(): number {
|
||||||
|
// This has to be done manually because Ionic's overlay mechanisms are not exposed externally, thus making it more difficult
|
||||||
|
// to implement custom overlays.
|
||||||
|
//
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// See https://github.com/ionic-team/ionic-framework/blob/a9b12a5aa4c150a1f8a80a826dda0df350bc0092/core/src/utils/overlays.ts#L39
|
||||||
|
|
||||||
|
const overlays = document.querySelectorAll<HTMLElement>(
|
||||||
|
'ion-action-sheet, ion-alert, ion-loading, ion-modal, ion-picker, ion-popover, ion-toast',
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(overlays).reduce((maxIndex, element) => {
|
||||||
|
const index = parseInt(element.style.zIndex);
|
||||||
|
|
||||||
|
if (isNaN(index)) {
|
||||||
|
return maxIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(maxIndex, index % 10000);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a sheet modal component.
|
||||||
|
*
|
||||||
|
* @param component Component to render inside the modal.
|
||||||
|
* @returns Modal result once it's been closed.
|
||||||
|
*/
|
||||||
|
async openSheet<T>(component: Constructor<CoreModalComponent<T>>): Promise<T> {
|
||||||
|
const container = document.querySelector('ion-app') ?? document.body;
|
||||||
|
const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
|
||||||
|
const element = await AngularFrameworkDelegate.attachViewToDom(
|
||||||
|
container,
|
||||||
|
CoreSheetModalComponent,
|
||||||
|
{ component },
|
||||||
|
);
|
||||||
|
const sheetModal = CoreDirectivesRegistry.require<CoreSheetModalComponent<CoreModalComponent<T>>>(
|
||||||
|
element,
|
||||||
|
CoreSheetModalComponent,
|
||||||
|
);
|
||||||
|
const modal = await sheetModal.show();
|
||||||
|
|
||||||
|
viewContainer?.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
modal.result.finally(async () => {
|
||||||
|
await sheetModal.hide();
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
viewContainer?.removeAttribute('aria-hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
return modal.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoreModals = makeSingleton(CoreModalsService);
|
Loading…
Reference in New Issue