MOBILE-3153 usertours: Implement User Tours
parent
cd323719db
commit
58e6be64e4
|
@ -139,7 +139,6 @@ const appConfig = {
|
||||||
'always',
|
'always',
|
||||||
],
|
],
|
||||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||||
'@typescript-eslint/unified-signatures': 'error',
|
|
||||||
'header/header': [
|
'header/header': [
|
||||||
2,
|
2,
|
||||||
'line',
|
'line',
|
||||||
|
|
|
@ -137,6 +137,13 @@ export class CoreDatabaseTableProxy<
|
||||||
return this.target.hasAny(conditions);
|
return this.target.hasAny(conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> {
|
||||||
|
return this.target.hasAnyByPrimaryKey(primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -219,6 +219,23 @@ export class CoreDatabaseTable<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the table has any record matching the given primary key.
|
||||||
|
*
|
||||||
|
* @param primaryKey Record primary key.
|
||||||
|
* @returns Whether the table contains a record matching the given primary key.
|
||||||
|
*/
|
||||||
|
async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.getOneByPrimaryKey(primaryKey);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Couldn't get the record.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count records in table.
|
* Count records in table.
|
||||||
*
|
*
|
||||||
|
|
|
@ -129,6 +129,15 @@ export class CoreDebugDatabaseTable<
|
||||||
return this.target.hasAny(conditions);
|
return this.target.hasAny(conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> {
|
||||||
|
this.logger.log('hasAnyByPrimaryKey', primaryKey);
|
||||||
|
|
||||||
|
return this.target.hasAnyByPrimaryKey(primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -134,6 +134,13 @@ export class CoreEagerDatabaseTable<
|
||||||
: Object.values(this.records).length > 0;
|
: Object.values(this.records).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> {
|
||||||
|
return this.serializePrimaryKey(primaryKey) in this.records;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -125,6 +125,15 @@ export class CoreLazyDatabaseTable<
|
||||||
return super.hasAny(conditions);
|
return super.hasAny(conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> {
|
||||||
|
const record = this.records[this.serializePrimaryKey(primaryKey)] ?? null;
|
||||||
|
|
||||||
|
return record !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
||||||
import { CoreSitePluginsModule } from './siteplugins/siteplugins.module';
|
import { CoreSitePluginsModule } from './siteplugins/siteplugins.module';
|
||||||
import { CoreStylesModule } from './styles/styles.module';
|
import { CoreStylesModule } from './styles/styles.module';
|
||||||
import { CoreTagModule } from './tag/tag.module';
|
import { CoreTagModule } from './tag/tag.module';
|
||||||
|
import { CoreUserToursModule } from './usertours/user-tours.module';
|
||||||
import { CoreUserModule } from './user/user.module';
|
import { CoreUserModule } from './user/user.module';
|
||||||
import { CoreViewerModule } from './viewer/viewer.module';
|
import { CoreViewerModule } from './viewer/viewer.module';
|
||||||
import { CoreXAPIModule } from './xapi/xapi.module';
|
import { CoreXAPIModule } from './xapi/xapi.module';
|
||||||
|
@ -66,6 +67,7 @@ import { CoreXAPIModule } from './xapi/xapi.module';
|
||||||
CoreSitePluginsModule,
|
CoreSitePluginsModule,
|
||||||
CoreTagModule,
|
CoreTagModule,
|
||||||
CoreStylesModule,
|
CoreStylesModule,
|
||||||
|
CoreUserToursModule,
|
||||||
CoreUserModule,
|
CoreUserModule,
|
||||||
CoreViewerModule,
|
CoreViewerModule,
|
||||||
CoreXAPIModule,
|
CoreXAPIModule,
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
// (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 { renderInlineStyles } from '@/core/utils/style-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to calculate layout styles for the focused area in a User Tour.
|
||||||
|
*/
|
||||||
|
export class CoreUserToursFocusLayout {
|
||||||
|
|
||||||
|
inlineStyles!: string;
|
||||||
|
|
||||||
|
private targetBoundingBox: DOMRect;
|
||||||
|
private targetComputedStyle: CSSStyleDeclaration;
|
||||||
|
|
||||||
|
constructor(target: HTMLElement) {
|
||||||
|
this.targetBoundingBox = target.getBoundingClientRect();
|
||||||
|
this.targetComputedStyle = window.getComputedStyle(target);
|
||||||
|
|
||||||
|
this.calculateStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate styles.
|
||||||
|
*/
|
||||||
|
private calculateStyles(): void {
|
||||||
|
this.inlineStyles = renderInlineStyles({
|
||||||
|
'top': this.targetBoundingBox.top,
|
||||||
|
'left': this.targetBoundingBox.left,
|
||||||
|
'width': this.targetBoundingBox.width,
|
||||||
|
'height': this.targetBoundingBox.height,
|
||||||
|
'border-top-left-radius': this.targetComputedStyle.borderTopLeftRadius,
|
||||||
|
'border-top-right-radius': this.targetComputedStyle.borderTopRightRadius,
|
||||||
|
'border-bottom-left-radius': this.targetComputedStyle.borderBottomLeftRadius,
|
||||||
|
'border-bottom-right-radius': this.targetComputedStyle.borderBottomRightRadius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
// (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 { CoreStyles, renderInlineStyles } from '@/core/utils/style-helpers';
|
||||||
|
import { Platform } from '@singletons';
|
||||||
|
import { CoreUserToursAlignment, CoreUserToursSide } from '../services/user-tours';
|
||||||
|
|
||||||
|
const ARROW_HEIGHT = 22;
|
||||||
|
const ARROW_WIDTH = 35;
|
||||||
|
const BORDER_RADIUS = 8;
|
||||||
|
const MARGIN = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to calculate layout styles for the popover wrapper in a User Tour.
|
||||||
|
*/
|
||||||
|
export class CoreUserToursPopoverLayout {
|
||||||
|
|
||||||
|
wrapperStyles: CoreStyles;
|
||||||
|
wrapperInlineStyles!: string;
|
||||||
|
wrapperArrowStyles: CoreStyles;
|
||||||
|
wrapperArrowInlineStyles!: string;
|
||||||
|
|
||||||
|
private targetBoundingBox: DOMRect;
|
||||||
|
private side: CoreUserToursSide;
|
||||||
|
private alignment: CoreUserToursAlignment;
|
||||||
|
|
||||||
|
constructor(target: HTMLElement, side: CoreUserToursSide, alignment: CoreUserToursAlignment) {
|
||||||
|
this.targetBoundingBox = target.getBoundingClientRect();
|
||||||
|
this.side = side;
|
||||||
|
this.alignment = alignment;
|
||||||
|
this.wrapperArrowStyles = {};
|
||||||
|
this.wrapperStyles = {};
|
||||||
|
|
||||||
|
this.calculateStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate styles.
|
||||||
|
*/
|
||||||
|
private calculateStyles(): void {
|
||||||
|
const sideHandlers: Record<CoreUserToursSide, () => void> = {
|
||||||
|
[CoreUserToursSide.Top]: this.calculateWrapperTopSideStyles,
|
||||||
|
[CoreUserToursSide.Bottom]: this.calculateWrapperBottomSideStyles,
|
||||||
|
[CoreUserToursSide.Right]: this.calculateWrapperRightSideStyles,
|
||||||
|
[CoreUserToursSide.Left]: this.calculateWrapperLeftSideStyles,
|
||||||
|
[CoreUserToursSide.Start]: Platform.isRTL ? this.calculateWrapperRightSideStyles : this.calculateWrapperLeftSideStyles,
|
||||||
|
[CoreUserToursSide.End]: Platform.isRTL ? this.calculateWrapperLeftSideStyles : this.calculateWrapperRightSideStyles,
|
||||||
|
};
|
||||||
|
|
||||||
|
sideHandlers[this.side].call(this);
|
||||||
|
|
||||||
|
this.wrapperInlineStyles = renderInlineStyles(this.wrapperStyles);
|
||||||
|
this.wrapperArrowInlineStyles = renderInlineStyles(this.wrapperArrowStyles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for an horizontal alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperHorizontalAlignmentStyles(): void {
|
||||||
|
const horizontalAlignmentHandlers: Record<CoreUserToursAlignment, () => void> ={
|
||||||
|
[CoreUserToursAlignment.Start]: Platform.isRTL
|
||||||
|
? this.calculateWrapperRightAlignmentStyles
|
||||||
|
: this.calculateWrapperLeftAlignmentStyles,
|
||||||
|
[CoreUserToursAlignment.Center]: this.calculateWrapperCenterHorizontalAlignmentStyles,
|
||||||
|
[CoreUserToursAlignment.End]: Platform.isRTL
|
||||||
|
? this.calculateWrapperLeftAlignmentStyles
|
||||||
|
: this.calculateWrapperRightAlignmentStyles,
|
||||||
|
};
|
||||||
|
|
||||||
|
horizontalAlignmentHandlers[this.alignment].call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for a vertical alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperVerticalAlignmentStyles(): void {
|
||||||
|
const verticalAlignmentHandlers: Record<CoreUserToursAlignment, () => void> ={
|
||||||
|
[CoreUserToursAlignment.Start]: this.calculateWrapperTopAlignmentStyles,
|
||||||
|
[CoreUserToursAlignment.Center]: this.calculateWrapperCenterVerticalAlignmentStyles,
|
||||||
|
[CoreUserToursAlignment.End]: this.calculateWrapperBottomAlignmentStyles,
|
||||||
|
};
|
||||||
|
|
||||||
|
verticalAlignmentHandlers[this.alignment].call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper arrow styles for an horizontal orientation.
|
||||||
|
*/
|
||||||
|
private calculateWrapperArrowHorizontalStyles(): void {
|
||||||
|
this.wrapperArrowStyles['border-width'] = `${ARROW_WIDTH / 2}px ${ARROW_HEIGHT}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper arrow styles for a vertical orientation.
|
||||||
|
*/
|
||||||
|
private calculateWrapperArrowVerticalStyles(): void {
|
||||||
|
this.wrapperArrowStyles['border-width'] = `${ARROW_HEIGHT}px ${ARROW_WIDTH / 2}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for a top side placement.
|
||||||
|
*/
|
||||||
|
private calculateWrapperTopSideStyles(): void {
|
||||||
|
this.wrapperStyles.bottom = window.innerHeight - this.targetBoundingBox.y + ARROW_HEIGHT + MARGIN;
|
||||||
|
this.wrapperArrowStyles.bottom = -ARROW_HEIGHT*2;
|
||||||
|
this.wrapperArrowStyles['border-top-color'] = 'var(--popover-background)';
|
||||||
|
|
||||||
|
this.calculateWrapperArrowVerticalStyles();
|
||||||
|
this.calculateWrapperHorizontalAlignmentStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for a bottom side placement.
|
||||||
|
*/
|
||||||
|
private calculateWrapperBottomSideStyles(): void {
|
||||||
|
this.wrapperStyles.top = this.targetBoundingBox.y + this.targetBoundingBox.height + ARROW_HEIGHT + MARGIN;
|
||||||
|
this.wrapperArrowStyles.top = -ARROW_HEIGHT*2;
|
||||||
|
this.wrapperArrowStyles['border-bottom-color'] = 'var(--popover-background)';
|
||||||
|
|
||||||
|
this.calculateWrapperArrowVerticalStyles();
|
||||||
|
this.calculateWrapperHorizontalAlignmentStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for a right side placement.
|
||||||
|
*/
|
||||||
|
private calculateWrapperRightSideStyles(): void {
|
||||||
|
this.wrapperStyles.left = this.targetBoundingBox.x + this.targetBoundingBox.width + ARROW_HEIGHT + MARGIN;
|
||||||
|
this.wrapperArrowStyles.left = -ARROW_HEIGHT*2;
|
||||||
|
this.wrapperArrowStyles['border-right-color'] = 'var(--popover-background)';
|
||||||
|
|
||||||
|
this.calculateWrapperArrowHorizontalStyles();
|
||||||
|
this.calculateWrapperVerticalAlignmentStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for a left side placement.
|
||||||
|
*/
|
||||||
|
private calculateWrapperLeftSideStyles(): void {
|
||||||
|
this.wrapperStyles.right = window.innerWidth - this.targetBoundingBox.x + ARROW_HEIGHT + MARGIN;
|
||||||
|
this.wrapperArrowStyles.right = -ARROW_HEIGHT*2;
|
||||||
|
this.wrapperArrowStyles['border-left-color'] = 'var(--popover-background)';
|
||||||
|
|
||||||
|
this.calculateWrapperArrowHorizontalStyles();
|
||||||
|
this.calculateWrapperVerticalAlignmentStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for top alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperTopAlignmentStyles() {
|
||||||
|
this.wrapperStyles.top = this.targetBoundingBox.y;
|
||||||
|
this.wrapperArrowStyles.top = BORDER_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for bottom alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperBottomAlignmentStyles(): void {
|
||||||
|
this.wrapperStyles.bottom = window.innerHeight - this.targetBoundingBox.y - this.targetBoundingBox.height;
|
||||||
|
this.wrapperArrowStyles.bottom = BORDER_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for right alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperRightAlignmentStyles() {
|
||||||
|
this.wrapperStyles.right = window.innerWidth - this.targetBoundingBox.x - this.targetBoundingBox.width;
|
||||||
|
this.wrapperArrowStyles.right = BORDER_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for left alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperLeftAlignmentStyles() {
|
||||||
|
this.wrapperStyles.left = this.targetBoundingBox.x;
|
||||||
|
this.wrapperArrowStyles.left = BORDER_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for center horizontal alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperCenterHorizontalAlignmentStyles() {
|
||||||
|
this.wrapperStyles.left = this.targetBoundingBox.x + this.targetBoundingBox.width / 2;
|
||||||
|
this.wrapperStyles.transform = 'translateX(-50%)';
|
||||||
|
this.wrapperStyles['transform-origin'] = '0 50%';
|
||||||
|
this.wrapperArrowStyles.left = '50%';
|
||||||
|
this.wrapperArrowStyles.transform = 'translateX(-50%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wrapper styles for center vertical alignment.
|
||||||
|
*/
|
||||||
|
private calculateWrapperCenterVerticalAlignmentStyles() {
|
||||||
|
this.wrapperStyles.top = this.targetBoundingBox.y + this.targetBoundingBox.height / 2;
|
||||||
|
this.wrapperStyles.transform = 'translateY(-50%)';
|
||||||
|
this.wrapperStyles['transform-origin'] = '50% 0';
|
||||||
|
this.wrapperArrowStyles.top = '50%';
|
||||||
|
this.wrapperArrowStyles.transform = 'translateY(-50%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreUserToursUserTourComponent } from './user-tour/user-tour';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tours components module.
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreUserToursUserTourComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreUserToursUserTourComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreUserToursComponentsModule {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div *ngIf="focusStyles" class="user-tour-focus" [style]="focusStyles"></div>
|
||||||
|
<div *ngIf="!focusStyles" class="user-tour-overlay"></div>
|
||||||
|
<div class="user-tour-wrapper" [style]="popoverWrapperStyles" #wrapper>
|
||||||
|
<span *ngIf="popover" class="user-tour-wrapper-arrow" [style]="popoverWrapperArrowStyles"></span>
|
||||||
|
</div>
|
|
@ -0,0 +1,61 @@
|
||||||
|
:host {
|
||||||
|
--popover-background: var(--ion-overlay-background-color, var(--ion-background-color, #fff));
|
||||||
|
|
||||||
|
z-index: 99;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.user-tour-focus {
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tour-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tour-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-popover .user-tour-wrapper {
|
||||||
|
color: var(--ion-text-color, #000);
|
||||||
|
background: var(--popover-background);
|
||||||
|
width: 70vw;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.user-tour-wrapper-arrow {
|
||||||
|
position: absolute;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.is-popover) .user-tour-wrapper {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body.dark) {
|
||||||
|
--popover-background: var(--gray-700);
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
// (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 { AfterViewInit, Component, ElementRef, HostBinding, Input, ViewChild } from '@angular/core';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreUserToursFocusLayout } from '@features/usertours/classes/focus-layout';
|
||||||
|
import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover-layout';
|
||||||
|
import {
|
||||||
|
CoreUserTours,
|
||||||
|
CoreUserToursAlignment,
|
||||||
|
CoreUserToursSide,
|
||||||
|
CoreUserToursStyle,
|
||||||
|
} from '@features/usertours/services/user-tours';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { AngularFrameworkDelegate } from '@singletons';
|
||||||
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour wrapper component.
|
||||||
|
*
|
||||||
|
* User Tours content will be rendered within this component according to the configured style.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-user-tours-user-tour',
|
||||||
|
templateUrl: 'core-user-tours-user-tour.html',
|
||||||
|
styleUrls: ['user-tour.scss'],
|
||||||
|
})
|
||||||
|
export class CoreUserToursUserTourComponent implements AfterViewInit {
|
||||||
|
|
||||||
|
@Input() container!: HTMLElement;
|
||||||
|
@Input() id!: string;
|
||||||
|
@Input() component!: unknown;
|
||||||
|
@Input() componentProps?: Record<string, unknown>;
|
||||||
|
@Input() focus?: HTMLElement;
|
||||||
|
@Input() style?: CoreUserToursStyle; // When this is undefined in a tour with a focused element, popover style will be used.
|
||||||
|
@Input() side?: CoreUserToursSide;
|
||||||
|
@Input() alignment?: CoreUserToursAlignment;
|
||||||
|
@HostBinding('class.is-active') active = false;
|
||||||
|
@HostBinding('class.is-popover') popover = false;
|
||||||
|
@ViewChild('wrapper') wrapper?: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
|
focusStyles?: string;
|
||||||
|
popoverWrapperStyles?: string;
|
||||||
|
popoverWrapperArrowStyles?: string;
|
||||||
|
private element: HTMLElement;
|
||||||
|
private wrapperTransform = '';
|
||||||
|
private wrapperElement = new CorePromisedValue<HTMLElement>();
|
||||||
|
|
||||||
|
constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
CoreComponentsRegistry.register(element, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (!this.wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wrapperElement.resolve(this.wrapper.nativeElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Present User Tour.
|
||||||
|
*/
|
||||||
|
async present(): Promise<void> {
|
||||||
|
// Insert tour component and wait until it's ready.
|
||||||
|
const wrapper = await this.wrapperElement;
|
||||||
|
const tour = await AngularFrameworkDelegate.attachViewToDom(wrapper, this.component, this.componentProps ?? {});
|
||||||
|
|
||||||
|
await CoreDomUtils.waitForImages(tour);
|
||||||
|
|
||||||
|
this.calculateStyles();
|
||||||
|
|
||||||
|
// Show tour.
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
await this.playEnterAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss User Tour.
|
||||||
|
*
|
||||||
|
* @param acknowledge Whether to confirm that the user has seen the User Tour.
|
||||||
|
*/
|
||||||
|
async dismiss(acknowledge: boolean = true): Promise<void> {
|
||||||
|
await this.playLeaveAnimation();
|
||||||
|
|
||||||
|
AngularFrameworkDelegate.removeViewFromDom(this.container, this.element);
|
||||||
|
|
||||||
|
acknowledge && CoreUserTours.acknowledge(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate inline styles.
|
||||||
|
*/
|
||||||
|
private calculateStyles(): void {
|
||||||
|
if (!this.focus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate focus styles.
|
||||||
|
const focusLayout = new CoreUserToursFocusLayout(this.focus);
|
||||||
|
|
||||||
|
this.focusStyles = focusLayout.inlineStyles;
|
||||||
|
|
||||||
|
// Calculate popup styles.
|
||||||
|
if ((this.style ?? CoreUserToursStyle.Popover) === CoreUserToursStyle.Popover) {
|
||||||
|
if (!this.side || !this.alignment) {
|
||||||
|
throw new Error('Cannot create a popover user tour without side and alignment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverLayout = new CoreUserToursPopoverLayout(this.focus, this.side, this.alignment);
|
||||||
|
|
||||||
|
this.popover = true;
|
||||||
|
this.popoverWrapperStyles = popoverLayout.wrapperInlineStyles;
|
||||||
|
this.popoverWrapperArrowStyles = popoverLayout.wrapperArrowInlineStyles;
|
||||||
|
this.wrapperTransform = `${popoverLayout.wrapperStyles.transform ?? ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play animation to show that the User Tour has started.
|
||||||
|
*/
|
||||||
|
private async playEnterAnimation(): Promise<void> {
|
||||||
|
const animations = [
|
||||||
|
this.element.animate({ opacity: ['0', '1'] }, { duration: ANIMATION_DURATION }),
|
||||||
|
this.wrapperElement.value?.animate(
|
||||||
|
{ transform: [`scale(1.2) ${this.wrapperTransform}`, `scale(1) ${this.wrapperTransform}`] },
|
||||||
|
{ duration: ANIMATION_DURATION },
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(animations.map(animation => animation?.finished));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play animation to show that the User Tour has endd.
|
||||||
|
*/
|
||||||
|
private async playLeaveAnimation(): Promise<void> {
|
||||||
|
const animations = [
|
||||||
|
this.element.animate({ opacity: ['1', '0'] }, { duration: ANIMATION_DURATION }),
|
||||||
|
this.wrapperElement.value?.animate(
|
||||||
|
{ transform: [`scale(1) ${this.wrapperTransform}`, `scale(1.2) ${this.wrapperTransform}`] },
|
||||||
|
{ duration: ANIMATION_DURATION },
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(animations.map(animation => animation?.finished));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// (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 { CoreAppSchema } from '@services/app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for CoreUserTours service.
|
||||||
|
*/
|
||||||
|
export const USER_TOURS_TABLE_NAME = 'user_tours';
|
||||||
|
export const APP_SCHEMA: CoreAppSchema = {
|
||||||
|
name: 'CoreUserTours',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: USER_TOURS_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'TEXT',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'acknowledgedTime',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tours database entry.
|
||||||
|
*/
|
||||||
|
export type CoreUserToursDBEntry = {
|
||||||
|
id: string;
|
||||||
|
acknowledgedTime: number;
|
||||||
|
};
|
|
@ -0,0 +1,257 @@
|
||||||
|
// (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 { asyncInstance } from '@/core/utils/async-instance';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
|
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AngularFrameworkDelegate, makeSingleton } from '@singletons';
|
||||||
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
|
import { CoreUserToursUserTourComponent } from '../components/user-tour/user-tour';
|
||||||
|
import { APP_SCHEMA, CoreUserToursDBEntry, USER_TOURS_TABLE_NAME } from './database/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to manage User Tours.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreUserToursService {
|
||||||
|
|
||||||
|
protected table = asyncInstance<CoreDatabaseTable<CoreUserToursDBEntry>>();
|
||||||
|
protected tours: CoreUserToursUserTourComponent[] = [];
|
||||||
|
protected tourReadyCallbacks = new WeakMap<CoreUserToursUserTourComponent, () => void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database.
|
||||||
|
*/
|
||||||
|
async initializeDatabase(): Promise<void> {
|
||||||
|
await CoreUtils.ignoreErrors(CoreApp.createTablesFromSchema(APP_SCHEMA));
|
||||||
|
|
||||||
|
this.table.setLazyConstructor(async () => {
|
||||||
|
const table = new CoreDatabaseTableProxy<CoreUserToursDBEntry>(
|
||||||
|
{ cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
||||||
|
CoreApp.getDB(),
|
||||||
|
USER_TOURS_TABLE_NAME,
|
||||||
|
['id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
return table;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a User Tour is pending or not.
|
||||||
|
*
|
||||||
|
* @param id User Tour id.
|
||||||
|
* @returns Whether the User Tour is pending or not.
|
||||||
|
*/
|
||||||
|
async isPending(id: string): Promise<boolean> {
|
||||||
|
const isAcknowledged = await this.table.hasAnyByPrimaryKey({ id });
|
||||||
|
|
||||||
|
return !isAcknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm that a User Tour has been seen by the user.
|
||||||
|
*
|
||||||
|
* @param id User Tour id.
|
||||||
|
*/
|
||||||
|
async acknowledge(id: string): Promise<void> {
|
||||||
|
await this.table.insert({ id, acknowledgedTime: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a User Tour if it's pending.
|
||||||
|
*
|
||||||
|
* @param options User Tour options.
|
||||||
|
*/
|
||||||
|
async showIfPending(options: CoreUserToursBasicOptions): Promise<void>;
|
||||||
|
async showIfPending(options: CoreUserToursPopoverFocusedOptions): Promise<void>;
|
||||||
|
async showIfPending(options: CoreUserToursOverlayFocusedOptions): Promise<void>;
|
||||||
|
async showIfPending(options: CoreUserToursOptions): Promise<void> {
|
||||||
|
const isPending = await CoreUserTours.isPending(options.id);
|
||||||
|
|
||||||
|
if (!isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.show(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a User Tour.
|
||||||
|
*
|
||||||
|
* @param options User Tour options.
|
||||||
|
*/
|
||||||
|
protected async show(options: CoreUserToursBasicOptions): Promise<void>;
|
||||||
|
protected async show(options: CoreUserToursPopoverFocusedOptions): Promise<void>;
|
||||||
|
protected async show(options: CoreUserToursOverlayFocusedOptions): Promise<void>;
|
||||||
|
protected async show(options: CoreUserToursOptions): Promise<void> {
|
||||||
|
const { delay, ...componentOptions } = options;
|
||||||
|
|
||||||
|
await CoreUtils.wait(delay ?? 200);
|
||||||
|
|
||||||
|
const container = document.querySelector('ion-app') ?? document.body;
|
||||||
|
const element = await AngularFrameworkDelegate.attachViewToDom(
|
||||||
|
container,
|
||||||
|
CoreUserToursUserTourComponent,
|
||||||
|
{ ...componentOptions, container },
|
||||||
|
);
|
||||||
|
const tour = CoreComponentsRegistry.require(element, CoreUserToursUserTourComponent);
|
||||||
|
|
||||||
|
this.tours.push(tour);
|
||||||
|
this.tours.length > 1
|
||||||
|
? await new Promise<void>(resolve => this.tourReadyCallbacks.set(tour, resolve))
|
||||||
|
: await tour.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the active User Tour, if any.
|
||||||
|
*
|
||||||
|
* @param acknowledge Whether to acknowledge that the user has seen this User Tour or not.
|
||||||
|
*/
|
||||||
|
async dismiss(acknowledge: boolean = true): Promise<void> {
|
||||||
|
if (this.tours.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTour = this.tours.shift() as CoreUserToursUserTourComponent;
|
||||||
|
const nextTour = this.tours[0] as CoreUserToursUserTourComponent | undefined;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
activeTour.dismiss(acknowledge),
|
||||||
|
nextTour?.present(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
nextTour && this.tourReadyCallbacks.get(nextTour)?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoreUserTours = makeSingleton(CoreUserToursService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour style.
|
||||||
|
*/
|
||||||
|
export const enum CoreUserToursStyle {
|
||||||
|
Overlay = 'overlay',
|
||||||
|
Popover = 'popover',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour side.
|
||||||
|
*/
|
||||||
|
export const enum CoreUserToursSide {
|
||||||
|
Top = 'top',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
Right = 'right',
|
||||||
|
Left = 'left',
|
||||||
|
Start = 'start',
|
||||||
|
End = 'end',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour alignment.
|
||||||
|
*/
|
||||||
|
export const enum CoreUserToursAlignment {
|
||||||
|
Start = 'start',
|
||||||
|
Center = 'center',
|
||||||
|
End = 'end',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic options to create a User Tour.
|
||||||
|
*/
|
||||||
|
export interface CoreUserToursBasicOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour component.
|
||||||
|
*/
|
||||||
|
component: unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties to pass to the User Tour component.
|
||||||
|
*/
|
||||||
|
componentProps?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Milliseconds to wait until the User Tour is shown.
|
||||||
|
*
|
||||||
|
* Defaults to 200ms.
|
||||||
|
*/
|
||||||
|
delay?: number;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to create a focused User Tour.
|
||||||
|
*/
|
||||||
|
export interface CoreUserToursFocusedOptions extends CoreUserToursBasicOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element to focus.
|
||||||
|
*/
|
||||||
|
focus: HTMLElement;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to create a focused User Tour using the Popover style.
|
||||||
|
*/
|
||||||
|
export interface CoreUserToursPopoverFocusedOptions extends CoreUserToursFocusedOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour style.
|
||||||
|
*/
|
||||||
|
style?: CoreUserToursStyle.Popover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position relative to the focused element.
|
||||||
|
*/
|
||||||
|
side: CoreUserToursSide;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alignment relative to the focused element.
|
||||||
|
*/
|
||||||
|
alignment: CoreUserToursAlignment;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to create a focused User Tour using the Overlay style.
|
||||||
|
*/
|
||||||
|
export interface CoreUserToursOverlayFocusedOptions extends CoreUserToursFocusedOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tour style.
|
||||||
|
*/
|
||||||
|
style: CoreUserToursStyle.Overlay;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to create a User Tour.
|
||||||
|
*/
|
||||||
|
export type CoreUserToursOptions =
|
||||||
|
CoreUserToursBasicOptions |
|
||||||
|
CoreUserToursPopoverFocusedOptions |
|
||||||
|
CoreUserToursOverlayFocusedOptions;
|
|
@ -0,0 +1,37 @@
|
||||||
|
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreUserToursComponentsModule } from './components/components.module';
|
||||||
|
import { CoreUserTours } from './services/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Tours module.
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreUserToursComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
useValue: async () => {
|
||||||
|
await CoreUserTours.initializeDatabase();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreUserToursModule {}
|
|
@ -47,6 +47,23 @@ export class CoreComponentsRegistry {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a component instances and fail if it cannot be resolved.
|
||||||
|
*
|
||||||
|
* @param element Root element.
|
||||||
|
* @param componentClass Component class.
|
||||||
|
* @returns Component instance.
|
||||||
|
*/
|
||||||
|
static require<T>(element: Element, componentClass?: ComponentConstructor<T>): T {
|
||||||
|
const instance = this.resolve(element, componentClass);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error('Couldn\'t resolve component instance');
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits all elements to be rendered.
|
* Waits all elements to be rendered.
|
||||||
*
|
*
|
||||||
|
|
|
@ -12,13 +12,22 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { AbstractType, ApplicationInitStatus, ApplicationRef, Injector, NgZone as NgZoneService, Type } from '@angular/core';
|
import {
|
||||||
|
AbstractType,
|
||||||
|
ApplicationInitStatus,
|
||||||
|
ApplicationRef,
|
||||||
|
ComponentFactoryResolver as ComponentFactoryResolverService,
|
||||||
|
Injector,
|
||||||
|
NgZone as NgZoneService,
|
||||||
|
Type,
|
||||||
|
} from '@angular/core';
|
||||||
import { Router as RouterService } from '@angular/router';
|
import { Router as RouterService } from '@angular/router';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser';
|
import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Platform as PlatformService,
|
Platform as PlatformService,
|
||||||
|
AngularDelegate as AngularDelegateService,
|
||||||
AlertController as AlertControllerService,
|
AlertController as AlertControllerService,
|
||||||
LoadingController as LoadingControllerService,
|
LoadingController as LoadingControllerService,
|
||||||
ModalController as ModalControllerService,
|
ModalController as ModalControllerService,
|
||||||
|
@ -58,11 +67,13 @@ import { Zip as ZipService } from '@ionic-native/zip/ngx';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreApplicationInitStatus } from '@classes/application-init-status';
|
import { CoreApplicationInitStatus } from '@classes/application-init-status';
|
||||||
|
import { asyncInstance } from '@/core/utils/async-instance';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injector instance used to resolve singletons.
|
* Injector instance used to resolve singletons.
|
||||||
*/
|
*/
|
||||||
let singletonsInjector: Injector | null = null;
|
const singletonsInjector = new CorePromisedValue<Injector>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create a method that proxies calls to the underlying singleton instance.
|
* Helper to create a method that proxies calls to the underlying singleton instance.
|
||||||
|
@ -87,7 +98,7 @@ export type CoreSingletonProxy<Service = unknown> = Service & {
|
||||||
* @param injector Module injector.
|
* @param injector Module injector.
|
||||||
*/
|
*/
|
||||||
export function setSingletonsInjector(injector: Injector): void {
|
export function setSingletonsInjector(injector: Injector): void {
|
||||||
singletonsInjector = injector;
|
singletonsInjector.resolve(injector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -127,11 +138,13 @@ export function makeSingleton<Service extends object = object>( // eslint-disabl
|
||||||
|
|
||||||
Object.defineProperty(singleton, 'instance', {
|
Object.defineProperty(singleton, 'instance', {
|
||||||
get: () => {
|
get: () => {
|
||||||
if (!singletonsInjector) {
|
const injector = singletonsInjector.value;
|
||||||
|
|
||||||
|
if (!injector) {
|
||||||
throw new Error('Can\'t resolve a singleton instance without an injector');
|
throw new Error('Can\'t resolve a singleton instance without an injector');
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = singletonsInjector.get(injectionToken);
|
const instance = injector.get(injectionToken);
|
||||||
|
|
||||||
singleton.setInstance(instance);
|
singleton.setInstance(instance);
|
||||||
|
|
||||||
|
@ -194,7 +207,9 @@ export const NgZone = makeSingleton(NgZoneService);
|
||||||
export const Http = makeSingleton(HttpClient);
|
export const Http = makeSingleton(HttpClient);
|
||||||
export const Platform = makeSingleton(PlatformService);
|
export const Platform = makeSingleton(PlatformService);
|
||||||
export const ActionSheetController = makeSingleton(ActionSheetControllerService);
|
export const ActionSheetController = makeSingleton(ActionSheetControllerService);
|
||||||
|
export const AngularDelegate = makeSingleton(AngularDelegateService);
|
||||||
export const AlertController = makeSingleton(AlertControllerService);
|
export const AlertController = makeSingleton(AlertControllerService);
|
||||||
|
export const ComponentFactoryResolver = makeSingleton(ComponentFactoryResolverService);
|
||||||
export const LoadingController = makeSingleton(LoadingControllerService);
|
export const LoadingController = makeSingleton(LoadingControllerService);
|
||||||
export const ModalController = makeSingleton(ModalControllerService);
|
export const ModalController = makeSingleton(ModalControllerService);
|
||||||
export const PopoverController = makeSingleton(PopoverControllerService);
|
export const PopoverController = makeSingleton(PopoverControllerService);
|
||||||
|
@ -208,3 +223,10 @@ export const DomSanitizer = makeSingleton(DomSanitizerService);
|
||||||
|
|
||||||
// Convert external libraries injectables.
|
// Convert external libraries injectables.
|
||||||
export const Translate = makeSingleton(TranslateService);
|
export const Translate = makeSingleton(TranslateService);
|
||||||
|
|
||||||
|
// Async singletons.
|
||||||
|
export const AngularFrameworkDelegate = asyncInstance(async () => {
|
||||||
|
const injector = await singletonsInjector;
|
||||||
|
|
||||||
|
return AngularDelegate.create(ComponentFactoryResolver.instance, injector);
|
||||||
|
});
|
||||||
|
|
|
@ -54,6 +54,20 @@ function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>):
|
||||||
|
|
||||||
promisedInstance.resolve(instance);
|
promisedInstance.resolve(instance);
|
||||||
},
|
},
|
||||||
|
setLazyConstructor(constructor) {
|
||||||
|
if (!promisedInstance) {
|
||||||
|
lazyConstructor = constructor;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!promisedInstance.isResolved()) {
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
Promise
|
||||||
|
.resolve(constructor())
|
||||||
|
.then(instance => promisedInstance?.isResolved() || promisedInstance?.resolve(instance));
|
||||||
|
}
|
||||||
|
},
|
||||||
resetInstance() {
|
resetInstance() {
|
||||||
if (!promisedInstance) {
|
if (!promisedInstance) {
|
||||||
return;
|
return;
|
||||||
|
@ -72,6 +86,7 @@ export interface AsyncInstanceWrapper<T> {
|
||||||
getInstance(): Promise<T>;
|
getInstance(): Promise<T>;
|
||||||
getProperty<P extends keyof T>(property: P): Promise<T[P]>;
|
getProperty<P extends keyof T>(property: P): Promise<T[P]>;
|
||||||
setInstance(instance: T): void;
|
setInstance(instance: T): void;
|
||||||
|
setLazyConstructor(lazyConstructor: () => T | Promise<T>): void;
|
||||||
resetInstance(): void;
|
resetInstance(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// (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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element styles.
|
||||||
|
*
|
||||||
|
* Number styles are interpreted as pixels; any other values should be set as a string.
|
||||||
|
*/
|
||||||
|
export type CoreStyles = Record<string, string | number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the given styles to be used inline on an element.
|
||||||
|
*
|
||||||
|
* @param styles Styles.
|
||||||
|
* @returns Inline styles.
|
||||||
|
*/
|
||||||
|
export function renderInlineStyles(styles: CoreStyles): string {
|
||||||
|
return Object
|
||||||
|
.entries(styles)
|
||||||
|
.reduce((renderedStyles, [property, value]) => {
|
||||||
|
const propertyValue = typeof value === 'string' ? value : `${value}px`;
|
||||||
|
|
||||||
|
return `${property}:${propertyValue};${renderedStyles}`;
|
||||||
|
}, '');
|
||||||
|
}
|
Loading…
Reference in New Issue