commit
1db5b85e40
|
@ -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',
|
||||||
|
|
|
@ -257,6 +257,7 @@ function build_lang($lang, $keys) {
|
||||||
$text = str_replace('$a->', '$a.', $text);
|
$text = str_replace('$a->', '$a.', $text);
|
||||||
$text = str_replace('{$a', '{{$a', $text);
|
$text = str_replace('{$a', '{{$a', $text);
|
||||||
$text = str_replace('}', '}}', $text);
|
$text = str_replace('}', '}}', $text);
|
||||||
|
$text = preg_replace('/@@.+?@@(<br>)?\\s*/', '', $text);
|
||||||
// Prevent double.
|
// Prevent double.
|
||||||
$text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
|
$text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1450,6 +1450,8 @@
|
||||||
"core.block.blocks": "moodle",
|
"core.block.blocks": "moodle",
|
||||||
"core.block.noblocks": "error",
|
"core.block.noblocks": "error",
|
||||||
"core.block.opendrawerblocks": "moodle",
|
"core.block.opendrawerblocks": "moodle",
|
||||||
|
"core.block.tour_navigation_dashboard_content": "tool_usertours",
|
||||||
|
"core.block.tour_navigation_dashboard_title": "tool_usertours",
|
||||||
"core.browser": "local_moodlemobileapp",
|
"core.browser": "local_moodlemobileapp",
|
||||||
"core.cancel": "moodle",
|
"core.cancel": "moodle",
|
||||||
"core.cannotconnect": "local_moodlemobileapp",
|
"core.cannotconnect": "local_moodlemobileapp",
|
||||||
|
@ -1567,6 +1569,8 @@
|
||||||
"core.course.startdate": "moodle",
|
"core.course.startdate": "moodle",
|
||||||
"core.course.thisweek": "format_weeks/currentsection",
|
"core.course.thisweek": "format_weeks/currentsection",
|
||||||
"core.course.todo": "completion",
|
"core.course.todo": "completion",
|
||||||
|
"core.course.tour_navigation_course_index_student_content": "tool_usertours",
|
||||||
|
"core.course.tour_navigation_course_index_student_title": "tool_usertours",
|
||||||
"core.course.useactivityonbrowser": "local_moodlemobileapp",
|
"core.course.useactivityonbrowser": "local_moodlemobileapp",
|
||||||
"core.course.viewcourse": "block_timeline",
|
"core.course.viewcourse": "block_timeline",
|
||||||
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
||||||
|
@ -2003,6 +2007,8 @@
|
||||||
"core.mainmenu.home": "moodle",
|
"core.mainmenu.home": "moodle",
|
||||||
"core.mainmenu.logout": "moodle",
|
"core.mainmenu.logout": "moodle",
|
||||||
"core.mainmenu.switchaccount": "local_moodlemobileapp",
|
"core.mainmenu.switchaccount": "local_moodlemobileapp",
|
||||||
|
"core.mainmenu.usermenutourdescription": "local_moodlemobileapp",
|
||||||
|
"core.mainmenu.usermenutourtitle": "local_moodlemobileapp",
|
||||||
"core.maxfilesize": "moodle",
|
"core.maxfilesize": "moodle",
|
||||||
"core.maxsizeandattachments": "moodle",
|
"core.maxsizeandattachments": "moodle",
|
||||||
"core.min": "moodle",
|
"core.min": "moodle",
|
||||||
|
@ -2263,6 +2269,7 @@
|
||||||
"core.submit": "moodle",
|
"core.submit": "moodle",
|
||||||
"core.success": "moodle",
|
"core.success": "moodle",
|
||||||
"core.summary": "moodle",
|
"core.summary": "moodle",
|
||||||
|
"core.swipenavigationtourdescription": "local_moodlemobileapp",
|
||||||
"core.tablet": "local_moodlemobileapp",
|
"core.tablet": "local_moodlemobileapp",
|
||||||
"core.tag.defautltagcoll": "tag",
|
"core.tag.defautltagcoll": "tag",
|
||||||
"core.tag.errorareanotsupported": "local_moodlemobileapp",
|
"core.tag.errorareanotsupported": "local_moodlemobileapp",
|
||||||
|
@ -2335,6 +2342,7 @@
|
||||||
"core.usernotfullysetup": "error",
|
"core.usernotfullysetup": "error",
|
||||||
"core.users": "moodle",
|
"core.users": "moodle",
|
||||||
"core.usersuspended": "tool_reportbuilder",
|
"core.usersuspended": "tool_reportbuilder",
|
||||||
|
"core.endonesteptour": "tool_usertours",
|
||||||
"core.view": "moodle",
|
"core.view": "moodle",
|
||||||
"core.viewcode": "local_moodlemobileapp",
|
"core.viewcode": "local_moodlemobileapp",
|
||||||
"core.vieweditor": "local_moodlemobileapp",
|
"core.vieweditor": "local_moodlemobileapp",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 206 143"><rect x=".5" y="11.017" width="180.187" height="112.617" rx="6.143" fill="#EAF6FF"/><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="11" width="170" height="113"><rect x="6.106" y="11.017" width="168.975" height="112.917" rx="6.143" fill="#EAF6FF"/></mask><g mask="url(#a)"><rect x="77.78" y="20.627" width="25.627" height="6.407" rx="3.203" fill="#86CBFF"/><rect x="28.529" y="37.444" width="124.129" height="10.411" rx="5.205" fill="#86CBFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.944 46.226a1.793 1.793 0 0 1-2.689.083l-4.859-5.178a1.793 1.793 0 1 1 2.615-2.454l3.533 3.765 3.646-4.05a1.793 1.793 0 0 1 2.665 2.4l-4.75 5.277c-.052.056-.105.108-.161.157Z" fill="#86CBFF"/><rect x="28.827" y="56.309" width="92.653" height="10.238" rx="5.119" fill="#C5E6FF"/><circle cx="17.821" cy="61.172" r="6.911" fill="#C5E6FF"/><rect x="28.827" y="76.537" width="78.32" height="10.238" rx="5.119" fill="#C5E6FF"/><circle cx="17.821" cy="81.4" r="6.911" fill="#C5E6FF"/><rect x="28.827" y="96.764" width="110.569" height="10.238" rx="5.119" fill="#C5E6FF"/><circle cx="17.821" cy="101.628" r="6.911" fill="#C5E6FF"/><rect x="28.827" y="116.992" width="90.605" height="10.238" rx="5.119" fill="#C5E6FF"/><circle cx="17.821" cy="121.855" r="6.911" fill="#C5E6FF"/></g><g filter="url(#b)"><rect x="155.45" y="90.299" width="48.05" height="48.05" rx="24.025" fill="#3880FF"/><path d="M170.466 106.952a1.638 1.638 0 1 0 0 3.276 1.638 1.638 0 0 0 0-3.276ZM174.561 107.362a1.228 1.228 0 1 0 0 2.457h14.742a1.229 1.229 0 1 0 0-2.457h-14.742ZM174.561 113.095a1.229 1.229 0 1 0 0 2.457h14.742a1.228 1.228 0 1 0 0-2.457h-14.742ZM173.332 120.057c0-.679.55-1.229 1.229-1.229h14.742a1.229 1.229 0 1 1 0 2.457h-14.742c-.679 0-1.229-.55-1.229-1.228ZM168.828 114.324a1.638 1.638 0 1 1 3.275 0 1.638 1.638 0 0 1-3.275 0ZM170.466 118.419a1.638 1.638 0 1 0 0 3.275 1.638 1.638 0 0 0 0-3.275Z" fill="#fff"/></g><path d="m166.471 0-11.559 20.02h23.118L166.471 0Zm-2.002 79.282a2.002 2.002 0 1 0 4.004 0h-4.004Zm0-69.844a2.002 2.002 0 1 0 4.004 0h-4.004Zm4.004 7.551a2.002 2.002 0 1 0-4.004 0h4.004Zm-4.004 18.877a2.002 2.002 0 1 0 4.004 0h-4.004Zm4.004 7.55a2.002 2.002 0 1 0-4.004 0h4.004Zm-4.004 18.877a2.002 2.002 0 1 0 4.004 0h-4.004Zm4.004 7.55a2.002 2.002 0 1 0-4.004 0h4.004Zm-4.004-52.854v18.877h4.004V16.989h-4.004Zm0 26.427v18.877h4.004V43.416h-4.004Zm0 26.428v9.438h4.004v-9.438h-4.004Z" fill="#0056B3"/><defs><filter id="b" x="153.266" y="90.299" width="52.418" height="52.418" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="2.184"/><feGaussianBlur stdDeviation="1.092"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.120972 0 0 0 0 0.203233 0 0 0 0 0.279167 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_319_9155"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_319_9155" result="shape"/></filter></defs></svg>
|
After Width: | Height: | Size: 3.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 98"><path fill-rule="evenodd" clip-rule="evenodd" d="M98.269 84.491c1.654.528 10.173 2.987 25.555 7.378l13.172-21.987 1.308-12.462-11.649-8.533-12.69-4.665 5.051-19.274-3.955-4.534-5.864 1.961-7.052 26.911-3.439-.148-5.382 2.166-4.591 11.492 7.54 15.381c-.324 3.682.342 5.787 1.996 6.314Z" fill="#fff"/><path d="M117.797 47.53c-1.169-.307-2.275-.156-3.317.452-1.043.608-1.717 1.494-2.022 2.66l-1.106 4.22-1.058-.277.803-3.067c.277-1.055 6.047-23.565 6.047-23.565.381-1.454-.597-3.654-2.779-4.226-2.182-.571-4.113.866-4.494 2.32-.381 1.455-1.345 5.078-1.61 6.09l-8.252 31.489-.8-1.267 1.487-5.672c.276-1.055.158-2.044-.354-2.965-.513-.922-1.298-1.52-2.356-1.798-1.015-.266-1.98-.132-2.898.403-.918.535-1.51 1.308-1.774 2.32l-1.935 7.387c-.27 1.033-.121 2.036.449 3.007l7.694 12.449c.636 1.083.772 2.316.409 3.702-.15.571-.07 1.12.239 1.648.31.528.751.867 1.324 1.017l21.166 5.546c.573.15 1.124.072 1.652-.236.528-.308.867-.748 1.017-1.32l.216-.824a9.733 9.733 0 0 1 .996-2.453l7.338-13.442c.428-.734.76-1.552.996-2.453l2.126-8.113c.276-1.055.158-2.043-.355-2.965-.512-.921-1.298-1.52-2.356-1.798-1.014-.266-1.98-.131-2.897.404-.918.534-1.509 1.308-1.774 2.319l-.277 1.055-1.058-.277 1.08-4.122a3.95 3.95 0 0 0-.201-2.608c-.364-.859-.952-1.489-1.766-1.89a5.8 5.8 0 0 0-.743-.266c-1.015-.265-1.981-.13-2.898.404-.918.535-1.509 1.308-1.774 2.32l-1.106 4.22-1.058-.277 1.054-4.023a4.577 4.577 0 0 0-.302-3.233 4.098 4.098 0 0 0-2.352-2.15c-.105-.05-.245-.1-.421-.145Zm1.105-4.222c1.852.486 3.351 1.466 4.496 2.94a8.08 8.08 0 0 1 4.361-.055 8.136 8.136 0 0 1 3.438 1.853 8.036 8.036 0 0 1 2.187 3.251 8.043 8.043 0 0 1 2.012.28c2.205.579 3.873 1.856 5.004 3.832 1.132 1.977 1.41 4.064.834 6.263l-2.126 8.112c-.293 1.121-.748 2.271-1.363 3.45l-7.371 13.433c-.271.493-.541 1.256-.812 2.29-.461 1.759-1.47 3.092-3.027 4-1.557.907-3.218 1.13-4.981.668l-21.165-5.547c-1.852-.485-3.205-1.527-4.059-3.125-.853-1.598-1.038-3.32-.554-5.168l-7.629-12.43c-1.255-2.045-1.589-4.188-1.001-6.43l1.935-7.388c.57-2.176 1.835-3.836 3.796-4.978 1.96-1.143 4.032-1.428 6.214-.856.243.063.951.26 1.056.311.679-2.054 5.921-22.284 6.778-25.557.929-3.545 6.497-4.029 8.679-3.457 2.182.572 6.916 3.269 5.868 7.269-.191.727-4.365 16.347-4.461 16.71 0 0-.357-.26 1.891.33Z" fill="#0056B3"/><path d="M101.728 32.565c-.812.436-1.832.135-2.178-.719a15.246 15.246 0 0 1-1.051-7.043 15.418 15.418 0 0 1 3.22-8.154 15.566 15.566 0 0 1 7.157-5.113 15.504 15.504 0 0 1 8.774-.415 15.306 15.306 0 0 1 7.547 4.42 15.209 15.209 0 0 1 3.872 7.819c.5 2.93.132 5.947-1.057 8.685a15.51 15.51 0 0 1-4.319 5.75c-.702.582-1.735.37-2.242-.39-.507-.76-.292-1.78.394-2.386a12.146 12.146 0 0 0 3.112-4.273 12.014 12.014 0 0 0 .829-6.81 11.927 11.927 0 0 0-3.036-6.13 12.005 12.005 0 0 0-5.917-3.465 12.158 12.158 0 0 0-6.88.324 12.207 12.207 0 0 0-5.612 4.01 12.092 12.092 0 0 0-2.525 6.393 11.95 11.95 0 0 0 .695 5.187c.319.864.028 1.873-.783 2.31Z" fill="#86CBFF"/><rect x="76.039" y="12.141" width="23.308" height="3.759" rx="1.88" fill="#86CBFF"/><rect x="70.619" y="21.163" width="21.805" height="3.759" rx="1.88" fill="#86CBFF"/><rect x="80.394" y="30.186" width="16.541" height="3.759" rx="1.88" fill="#86CBFF"/><path d="M141.5 20.788a1.88 1.88 0 1 0 0 3.759v-3.76Zm60.15 1.88-18.797-10.853V33.52l18.797-10.853Zm-60.15 1.879h43.233v-3.76H141.5v3.76ZM61.15 24.547a1.88 1.88 0 0 0 0-3.76v3.76ZM1 22.667 19.797 33.52V11.815L1 22.667Zm60.15-1.88H17.917v3.76H61.15v-3.76Z" fill="#0056B3"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 269 173"><path d="M130.298 171.955a1.999 1.999 0 1 0 2.404-3.196l-2.404 3.196Zm137.887-153.65a2 2 0 0 0 .426-2.796l-10.665-14.5a2 2 0 1 0-3.222 2.37l9.48 12.89-12.889 9.48a2 2 0 1 0 2.37 3.222l14.5-10.665ZM124.847 36.115c14.837-13.515 33.816-20.396 57.372-22.633 23.592-2.242 51.644.187 84.48 5.19l.602-3.954c-32.918-5.016-61.365-7.507-85.46-5.218-24.131 2.292-44.025 9.391-59.688 23.657l2.694 2.958Zm34.44 79.302c-13.382 1.869-23.94-.552-31.827-5.493-7.898-4.946-13.278-12.519-16.124-21.225-5.717-17.49-1.113-39.265 13.511-52.584l-2.694-2.958c-15.876 14.46-20.786 37.916-14.619 56.785 3.096 9.473 9.008 17.864 17.803 23.372 8.804 5.515 20.34 8.043 34.504 6.064l-.554-3.961Zm-26.585 53.343c-25.76-19.374-35.531-44.979-33.124-66.267 2.404-21.255 16.912-38.345 40.27-41.227l-.49-3.97c-25.369 3.13-41.163 21.836-43.754 44.747-2.587 22.876 7.954 49.803 34.694 69.913l2.404-3.196Zm7.146-107.494c23.592-2.91 41.413 9.316 46.813 22.682 2.682 6.636 2.305 13.48-1.745 19.109-4.087 5.679-12.172 10.48-25.629 12.36l.554 3.961c14.089-1.968 23.372-7.107 28.321-13.985 4.986-6.928 5.312-15.26 2.208-22.943-6.171-15.272-25.876-28.255-51.012-25.154l.49 3.97Z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -60,6 +60,7 @@ import { CoreSpacerComponent } from './spacer/spacer';
|
||||||
import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
|
import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
|
||||||
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
||||||
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||||
|
import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -102,6 +103,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||||
CoreComboboxComponent,
|
CoreComboboxComponent,
|
||||||
CoreSpacerComponent,
|
CoreSpacerComponent,
|
||||||
CoreHorizontalScrollControlsComponent,
|
CoreHorizontalScrollControlsComponent,
|
||||||
|
CoreSwipeNavigationTourComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -151,6 +153,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||||
CoreComboboxComponent,
|
CoreComboboxComponent,
|
||||||
CoreSpacerComponent,
|
CoreSpacerComponent,
|
||||||
CoreHorizontalScrollControlsComponent,
|
CoreHorizontalScrollControlsComponent,
|
||||||
|
CoreSwipeNavigationTourComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<img src="assets/img/user-tours/swipe-navigation.svg" alt="" />
|
||||||
|
<p>{{ 'core.swipenavigationtourdescription' | translate }}</p>
|
||||||
|
<ion-button (click)="dismiss()" expand="block">
|
||||||
|
{{ 'core.endonesteptour' | translate }}
|
||||||
|
</ion-button>
|
|
@ -0,0 +1,12 @@
|
||||||
|
:host {
|
||||||
|
max-width: 85vw;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 { Component } from '@angular/core';
|
||||||
|
import { CoreUserTours } from '@features/usertours/services/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component showing the User Tour for the Swipe Navigation feature.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-swipe-navigation-tour',
|
||||||
|
templateUrl: 'core-swipe-navigation-tour.html',
|
||||||
|
styleUrls: ['swipe-navigation-tour.scss'],
|
||||||
|
})
|
||||||
|
export class CoreSwipeNavigationTourComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss User Tour.
|
||||||
|
*/
|
||||||
|
async dismiss(): Promise<void> {
|
||||||
|
await CoreUserTours.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -190,6 +190,9 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
};
|
};
|
||||||
|
|
||||||
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
||||||
|
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
||||||
|
|
||||||
|
onOutletUpdated();
|
||||||
|
|
||||||
onOutletUpdated();
|
onOutletUpdated();
|
||||||
|
|
||||||
|
@ -311,6 +314,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
* @param content Content element.
|
* @param content Content element.
|
||||||
*/
|
*/
|
||||||
protected updateContent(content?: HTMLIonContentElement | null): void {
|
protected updateContent(content?: HTMLIonContentElement | null): void {
|
||||||
|
if (content === (this.content ?? null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.content) {
|
if (this.content) {
|
||||||
if (this.contentScrollListener) {
|
if (this.contentScrollListener) {
|
||||||
this.content.removeEventListener('ionScroll', this.contentScrollListener);
|
this.content.removeEventListener('ionScroll', this.contentScrollListener);
|
||||||
|
@ -321,6 +328,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
|
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
|
||||||
delete this.endContentScrollListener;
|
delete this.endContentScrollListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.content;
|
delete this.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { CoreSwipeNavigationDirective } from './swipe-navigation';
|
||||||
import { CoreCollapsibleItemDirective } from './collapsible-item';
|
import { CoreCollapsibleItemDirective } from './collapsible-item';
|
||||||
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
|
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
|
||||||
import { CoreContentDirective } from './content';
|
import { CoreContentDirective } from './content';
|
||||||
|
import { CoreOnAppearDirective } from './on-appear';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -46,6 +47,7 @@ import { CoreContentDirective } from './content';
|
||||||
CoreSupressEventsDirective,
|
CoreSupressEventsDirective,
|
||||||
CoreUserLinkDirective,
|
CoreUserLinkDirective,
|
||||||
CoreAriaButtonClickDirective,
|
CoreAriaButtonClickDirective,
|
||||||
|
CoreOnAppearDirective,
|
||||||
CoreOnResizeDirective,
|
CoreOnResizeDirective,
|
||||||
CoreDownloadFileDirective,
|
CoreDownloadFileDirective,
|
||||||
CoreCollapsibleHeaderDirective,
|
CoreCollapsibleHeaderDirective,
|
||||||
|
@ -66,6 +68,7 @@ import { CoreContentDirective } from './content';
|
||||||
CoreSupressEventsDirective,
|
CoreSupressEventsDirective,
|
||||||
CoreUserLinkDirective,
|
CoreUserLinkDirective,
|
||||||
CoreAriaButtonClickDirective,
|
CoreAriaButtonClickDirective,
|
||||||
|
CoreOnAppearDirective,
|
||||||
CoreOnResizeDirective,
|
CoreOnResizeDirective,
|
||||||
CoreDownloadFileDirective,
|
CoreDownloadFileDirective,
|
||||||
CoreCollapsibleHeaderDirective,
|
CoreCollapsibleHeaderDirective,
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
// (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 { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to listen when an element becomes visible.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[onAppear]',
|
||||||
|
})
|
||||||
|
export class CoreOnAppearDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Output() onAppear = new EventEmitter();
|
||||||
|
|
||||||
|
private element: HTMLElement;
|
||||||
|
private interval?: number;
|
||||||
|
|
||||||
|
constructor(element: ElementRef) {
|
||||||
|
this.element = element.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.interval = window.setInterval(() => {
|
||||||
|
if (!CoreDomUtils.isElementVisible(this.element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onAppear.emit();
|
||||||
|
window.clearInterval(this.interval);
|
||||||
|
|
||||||
|
delete this.interval;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.interval && window.clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core';
|
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core';
|
||||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||||
|
import { CoreSwipeNavigationTourComponent } from '@components/swipe-navigation-tour/swipe-navigation-tour';
|
||||||
|
import { CoreUserTours } from '@features/usertours/services/user-tours';
|
||||||
import { Gesture, GestureDetail } from '@ionic/angular';
|
import { Gesture, GestureDetail } from '@ionic/angular';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { GestureController, Platform } from '@singletons';
|
import { GestureController, Platform } from '@singletons';
|
||||||
|
@ -50,7 +52,8 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
// Set up gesture listener
|
||||||
const style = this.element.style;
|
const style = this.element.style;
|
||||||
this.swipeGesture = GestureController.create({
|
this.swipeGesture = GestureController.create({
|
||||||
el: this.element,
|
el: this.element,
|
||||||
|
@ -71,6 +74,26 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.swipeGesture.enable();
|
this.swipeGesture.enable();
|
||||||
|
|
||||||
|
// Show user tour.
|
||||||
|
const source = this.manager?.getSource();
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await source.waitForLoaded();
|
||||||
|
|
||||||
|
const items = source.getItems() ?? [];
|
||||||
|
|
||||||
|
if (items.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUserTours.showIfPending({
|
||||||
|
id: 'swipe-navigation',
|
||||||
|
component: CoreSwipeNavigationTourComponent,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks';
|
import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks';
|
||||||
import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button';
|
import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button';
|
||||||
|
import { CoreBlockSideBlocksTourComponent } from './side-blocks-tour/side-blocks-tour';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -27,6 +28,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl
|
||||||
CoreBlockPreRenderedComponent,
|
CoreBlockPreRenderedComponent,
|
||||||
CoreBlockSideBlocksComponent,
|
CoreBlockSideBlocksComponent,
|
||||||
CoreBlockSideBlocksButtonComponent,
|
CoreBlockSideBlocksButtonComponent,
|
||||||
|
CoreBlockSideBlocksTourComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
@ -37,6 +39,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl
|
||||||
CoreBlockPreRenderedComponent,
|
CoreBlockPreRenderedComponent,
|
||||||
CoreBlockSideBlocksComponent,
|
CoreBlockSideBlocksComponent,
|
||||||
CoreBlockSideBlocksButtonComponent,
|
CoreBlockSideBlocksButtonComponent,
|
||||||
|
CoreBlockSideBlocksTourComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreBlockComponentsModule {}
|
export class CoreBlockComponentsModule {}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
<ion-button (click)="openBlocks()" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary">
|
<ion-button (click)="openBlocks()" (onAppear)="showTour()" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary"
|
||||||
|
#button>
|
||||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
// 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 { Component, Input } from '@angular/core';
|
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||||
|
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreBlockSideBlocksTourComponent } from '../side-blocks-tour/side-blocks-tour';
|
||||||
import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
|
import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,6 +29,7 @@ import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
|
||||||
export class CoreBlockSideBlocksButtonComponent {
|
export class CoreBlockSideBlocksButtonComponent {
|
||||||
|
|
||||||
@Input() courseId!: number;
|
@Input() courseId!: number;
|
||||||
|
@ViewChild('button', { read: ElementRef }) button?: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open side blocks.
|
* Open side blocks.
|
||||||
|
@ -40,4 +43,23 @@ export class CoreBlockSideBlocksButtonComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show User Tour.
|
||||||
|
*/
|
||||||
|
async showTour(): Promise<void> {
|
||||||
|
const nativeButton = this.button?.nativeElement.shadowRoot?.children[0] as HTMLElement;
|
||||||
|
|
||||||
|
if (!nativeButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUserTours.showIfPending({
|
||||||
|
id: 'side-blocks-button',
|
||||||
|
component: CoreBlockSideBlocksTourComponent,
|
||||||
|
focus: nativeButton,
|
||||||
|
side: CoreUserToursSide.Start,
|
||||||
|
alignment: CoreUserToursAlignment.Center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<h2>{{ 'core.block.tour_navigation_dashboard_title' | translate }}</h2>
|
||||||
|
<img src="assets/img/user-tours/side-blocks.svg" alt="" />
|
||||||
|
<p>{{ 'core.block.tour_navigation_dashboard_content' | translate }}</p>
|
||||||
|
<ion-button (click)="dismiss()" expand="block">
|
||||||
|
{{ 'core.endonesteptour' | translate }}
|
||||||
|
</ion-button>
|
|
@ -0,0 +1,15 @@
|
||||||
|
:host {
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 { Component } from '@angular/core';
|
||||||
|
import { CoreUserTours } from '@features/usertours/services/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component showing the User Tour for the Side Blocks feature.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-block-side-blocks-tour',
|
||||||
|
templateUrl: 'side-blocks-tour.html',
|
||||||
|
styleUrls: ['side-blocks-tour.scss'],
|
||||||
|
})
|
||||||
|
export class CoreBlockSideBlocksTourComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss User Tour.
|
||||||
|
*/
|
||||||
|
async dismiss(): Promise<void> {
|
||||||
|
await CoreUserTours.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"blocks": "Blocks",
|
"blocks": "Blocks",
|
||||||
"noblocks": "No blocks found!",
|
"noblocks": "No blocks found!",
|
||||||
"opendrawerblocks": "Open block drawer"
|
"opendrawerblocks": "Open block drawer",
|
||||||
|
"tour_navigation_dashboard_content": "This side panel can contain more features.",
|
||||||
|
"tour_navigation_dashboard_title": "Expand to explore"
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { CoreCourseModuleInfoComponent } from './module-info/module-info';
|
||||||
import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion';
|
import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion';
|
||||||
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
|
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
|
||||||
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
||||||
|
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -39,6 +40,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar
|
||||||
CoreCourseModuleInfoComponent,
|
CoreCourseModuleInfoComponent,
|
||||||
CoreCourseModuleManualCompletionComponent,
|
CoreCourseModuleManualCompletionComponent,
|
||||||
CoreCourseCourseIndexComponent,
|
CoreCourseCourseIndexComponent,
|
||||||
|
CoreCourseCourseIndexTourComponent,
|
||||||
CoreCourseTagAreaComponent,
|
CoreCourseTagAreaComponent,
|
||||||
CoreCourseUnsupportedModuleComponent,
|
CoreCourseUnsupportedModuleComponent,
|
||||||
CoreCourseModuleNavigationComponent,
|
CoreCourseModuleNavigationComponent,
|
||||||
|
@ -57,6 +59,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar
|
||||||
CoreCourseModuleInfoComponent,
|
CoreCourseModuleInfoComponent,
|
||||||
CoreCourseModuleManualCompletionComponent,
|
CoreCourseModuleManualCompletionComponent,
|
||||||
CoreCourseCourseIndexComponent,
|
CoreCourseCourseIndexComponent,
|
||||||
|
CoreCourseCourseIndexTourComponent,
|
||||||
CoreCourseTagAreaComponent,
|
CoreCourseTagAreaComponent,
|
||||||
CoreCourseUnsupportedModuleComponent,
|
CoreCourseUnsupportedModuleComponent,
|
||||||
CoreCourseModuleNavigationComponent,
|
CoreCourseModuleNavigationComponent,
|
||||||
|
|
|
@ -49,7 +49,8 @@
|
||||||
|
|
||||||
<!-- Course Index button. -->
|
<!-- Course Index button. -->
|
||||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex">
|
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex">
|
||||||
<ion-fab-button (click)="openCourseIndex()" [attr.aria-label]="'core.course.courseindex' | translate" color="secondary">
|
<ion-fab-button (click)="openCourseIndex()" (onAppear)="showCourseIndexTour()" [attr.aria-label]="'core.course.courseindex' | translate"
|
||||||
|
color="secondary" #courseIndexFab>
|
||||||
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
|
||||||
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
||||||
</ion-fab-button>
|
</ion-fab-button>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
QueryList,
|
QueryList,
|
||||||
Type,
|
Type,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||||
|
@ -44,6 +45,8 @@ import { CoreBlockHelper } from '@features/block/services/block-helper';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
|
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
|
||||||
|
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
|
||||||
|
import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||||
|
@ -71,6 +74,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||||
|
|
||||||
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
||||||
|
@ViewChild('courseIndexFab', { read: ElementRef }) courseIndexFab?: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
// All the possible component classes.
|
// All the possible component classes.
|
||||||
courseFormatComponent?: Type<unknown>;
|
courseFormatComponent?: Type<unknown>;
|
||||||
|
@ -160,6 +164,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Course Index User Tour.
|
||||||
|
*/
|
||||||
|
async showCourseIndexTour(): Promise<void> {
|
||||||
|
const nativeButton = this.courseIndexFab?.nativeElement.shadowRoot?.children[0] as HTMLElement;
|
||||||
|
|
||||||
|
if (!nativeButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUserTours.showIfPending({
|
||||||
|
id: 'course-index',
|
||||||
|
component: CoreCourseCourseIndexTourComponent,
|
||||||
|
focus: nativeButton,
|
||||||
|
side: CoreUserToursSide.Top,
|
||||||
|
alignment: CoreUserToursAlignment.End,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect changes on input properties.
|
* Detect changes on input properties.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<h2>{{ 'core.course.tour_navigation_course_index_student_title' | translate }}</h2>
|
||||||
|
<img src="assets/img/user-tours/course-index.svg" alt="" />
|
||||||
|
<p>{{ 'core.course.tour_navigation_course_index_student_content' | translate }}</p>
|
||||||
|
<ion-button (click)="dismiss()" expand="block">
|
||||||
|
{{ 'core.endonesteptour' | translate }}
|
||||||
|
</ion-button>
|
|
@ -0,0 +1,15 @@
|
||||||
|
:host {
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 { Component } from '@angular/core';
|
||||||
|
import { CoreUserTours } from '@features/usertours/services/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component showing the User Tour for the Course Index feature.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-course-index-tour',
|
||||||
|
templateUrl: 'course-index-tour.html',
|
||||||
|
styleUrls: ['course-index-tour.scss'],
|
||||||
|
})
|
||||||
|
export class CoreCourseCourseIndexTourComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the User Tour.
|
||||||
|
*/
|
||||||
|
async dismiss(): Promise<void> {
|
||||||
|
await CoreUserTours.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -54,6 +54,8 @@
|
||||||
"startdate": "Course start date",
|
"startdate": "Course start date",
|
||||||
"thisweek": "This week",
|
"thisweek": "This week",
|
||||||
"todo": "To do",
|
"todo": "To do",
|
||||||
|
"tour_navigation_course_index_student_content": "Browse through activities and track your progress.",
|
||||||
|
"tour_navigation_course_index_student_title": "Find your way around",
|
||||||
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
||||||
"viewcourse": "View course",
|
"viewcourse": "View course",
|
||||||
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -17,11 +17,13 @@ import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button';
|
import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button';
|
||||||
import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
|
import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
|
||||||
import { CoreLoginComponentsModule } from '@features/login/components/components.module';
|
import { CoreLoginComponentsModule } from '@features/login/components/components.module';
|
||||||
|
import { CoreMainMenuUserMenuTourComponent } from './user-menu-tour/user-menu-tour';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreMainMenuUserButtonComponent,
|
CoreMainMenuUserButtonComponent,
|
||||||
CoreMainMenuUserMenuComponent,
|
CoreMainMenuUserMenuComponent,
|
||||||
|
CoreMainMenuUserMenuTourComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
@ -30,6 +32,7 @@ import { CoreLoginComponentsModule } from '@features/login/components/components
|
||||||
exports: [
|
exports: [
|
||||||
CoreMainMenuUserButtonComponent,
|
CoreMainMenuUserButtonComponent,
|
||||||
CoreMainMenuUserMenuComponent,
|
CoreMainMenuUserMenuComponent,
|
||||||
|
CoreMainMenuUserMenuTourComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreMainMenuComponentsModule {}
|
export class CoreMainMenuComponentsModule {}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
<core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false"
|
<core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false"
|
||||||
(ariaButtonClick)="openUserMenu($event)" role="button" tabindex="0" [attr.aria-label]="'core.user.useraccount' | translate">
|
(ariaButtonClick)="openUserMenu($event)" (onAppear)="showTour()" role="button" tabindex="0"
|
||||||
|
[attr.aria-label]="'core.user.useraccount' | translate" #avatar>
|
||||||
</core-user-avatar>
|
</core-user-avatar>
|
||||||
|
|
|
@ -12,11 +12,13 @@
|
||||||
// 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 { Component, OnInit } from '@angular/core';
|
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||||
import { CoreSiteInfo } from '@classes/site';
|
import { CoreSiteInfo } from '@classes/site';
|
||||||
|
import { CoreUserTours, CoreUserToursStyle } from '@features/usertours/services/user-tours';
|
||||||
import { IonRouterOutlet } from '@ionic/angular';
|
import { IonRouterOutlet } from '@ionic/angular';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreMainMenuUserMenuTourComponent } from '../user-menu-tour/user-menu-tour';
|
||||||
import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu';
|
import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +36,8 @@ export class CoreMainMenuUserButtonComponent implements OnInit {
|
||||||
siteInfo?: CoreSiteInfo;
|
siteInfo?: CoreSiteInfo;
|
||||||
isMainScreen = false;
|
isMainScreen = false;
|
||||||
|
|
||||||
|
@ViewChild('avatar', { read: ElementRef }) avatar?: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
constructor(protected routerOutlet: IonRouterOutlet) {
|
constructor(protected routerOutlet: IonRouterOutlet) {
|
||||||
const currentSite = CoreSites.getRequiredCurrentSite();
|
const currentSite = CoreSites.getRequiredCurrentSite();
|
||||||
|
|
||||||
|
@ -61,4 +65,20 @@ export class CoreMainMenuUserButtonComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show User Tour.
|
||||||
|
*/
|
||||||
|
async showTour(): Promise<void> {
|
||||||
|
if (!this.avatar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUserTours.showIfPending({
|
||||||
|
id: 'user-menu',
|
||||||
|
component: CoreMainMenuUserMenuTourComponent,
|
||||||
|
focus: this.avatar.nativeElement,
|
||||||
|
style: CoreUserToursStyle.Overlay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<img src="assets/img/user-tours/user-menu.svg" alt="" />
|
||||||
|
<h2>{{ 'core.mainmenu.usermenutourtitle' | translate }}</h2>
|
||||||
|
<p>{{ 'core.mainmenu.usermenutourdescription' | translate }}</p>
|
||||||
|
<ion-button (click)="dismiss()" expand="block">
|
||||||
|
{{ 'core.endonesteptour' | translate }}
|
||||||
|
</ion-button>
|
|
@ -0,0 +1,26 @@
|
||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
max-width: 85vw;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: calc(100vw - var(--core-avatar-size) * 2 - 16px);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context([dir=rtl]) img {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 { Component } from '@angular/core';
|
||||||
|
import { CoreUserTours } from '@features/usertours/services/user-tours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component showing the User Tour for the User Menu feature.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-mainmenu-user-menu-tour',
|
||||||
|
templateUrl: 'user-menu-tour.html',
|
||||||
|
styleUrls: ['user-menu-tour.scss'],
|
||||||
|
})
|
||||||
|
export class CoreMainMenuUserMenuTourComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the User Tour.
|
||||||
|
*/
|
||||||
|
async dismiss(): Promise<void> {
|
||||||
|
await CoreUserTours.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"switchaccount": "Switch account"
|
"switchaccount": "Switch account",
|
||||||
|
"usermenutourdescription": "The place to check your grades, change your preferences or switch accounts.",
|
||||||
|
"usermenutourtitle": "Explore your personal area"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,262 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
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> {
|
||||||
|
if (CoreConstants.CONFIG.disableUserTours || CoreConstants.CONFIG.disabledUserTours?.includes(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {}
|
|
@ -302,6 +302,7 @@
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
|
"swipenavigationtourdescription": "Swipe left and right to navigate around.",
|
||||||
"tablet": "Tablet",
|
"tablet": "Tablet",
|
||||||
"teachers": "Teachers",
|
"teachers": "Teachers",
|
||||||
"thereisdatatosync": "There are offline {{$a}} to be synchronised.",
|
"thereisdatatosync": "There are offline {{$a}} to be synchronised.",
|
||||||
|
@ -329,6 +330,7 @@
|
||||||
"usernotfullysetup": "User not fully set-up",
|
"usernotfullysetup": "User not fully set-up",
|
||||||
"usernologin": "Authentication has been revoked for this account",
|
"usernologin": "Authentication has been revoked for this account",
|
||||||
"usersuspended": "Registration suspended",
|
"usersuspended": "Registration suspended",
|
||||||
|
"endonesteptour": "Got it",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"viewcode": "View code",
|
"viewcode": "View code",
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreDatabaseTable } from '@classes/database/database-table';
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { asyncInstance } from '../utils/async-instance';
|
import { asyncInstance } from '../utils/async-instance';
|
||||||
import { CorePromisedValue } from '@classes/promised-value';
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { CoreUtils } from './utils/utils';
|
import { CoreBrowser } from '@singletons/browser';
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
@ -195,11 +195,11 @@ export class CoreConfigProvider {
|
||||||
* Load development config overrides.
|
* Load development config overrides.
|
||||||
*/
|
*/
|
||||||
protected loadDevelopmentConfig(): void {
|
protected loadDevelopmentConfig(): void {
|
||||||
if (!CoreConstants.enableDevTools() || !CoreUtils.hasCookie('MoodleAppConfig')) {
|
if (!CoreConstants.enableDevTools() || !CoreBrowser.hasCookie('MoodleAppConfig')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.patchEnvironment(JSON.parse(CoreUtils.getCookie('MoodleAppConfig') ?? '{}'));
|
this.patchEnvironment(JSON.parse(CoreBrowser.getCookie('MoodleAppConfig') ?? '{}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,9 +16,9 @@ import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { SQLiteDB } from '@classes/sqlitedb';
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb';
|
import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb';
|
||||||
|
import { CoreBrowser } from '@singletons/browser';
|
||||||
import { makeSingleton, SQLite, Platform } from '@singletons';
|
import { makeSingleton, SQLite, Platform } from '@singletons';
|
||||||
import { CoreAppProvider } from './app';
|
import { CoreAppProvider } from './app';
|
||||||
import { CoreUtils } from './utils/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service allows interacting with the local database to store and retrieve data.
|
* This service allows interacting with the local database to store and retrieve data.
|
||||||
|
@ -36,7 +36,7 @@ export class CoreDbProvider {
|
||||||
* @returns Whether queries should be logged.
|
* @returns Whether queries should be logged.
|
||||||
*/
|
*/
|
||||||
loggingEnabled(): boolean {
|
loggingEnabled(): boolean {
|
||||||
return CoreUtils.hasCookie('MoodleAppDBLoggingEnabled') || CoreAppProvider.isAutomated();
|
return CoreBrowser.hasCookie('MoodleAppDBLoggingEnabled') || CoreAppProvider.isAutomated();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -806,6 +806,24 @@ export class CoreDomUtilsProvider {
|
||||||
return elementPoint > window.innerHeight || elementPoint < scrollTopPos;
|
return elementPoint > window.innerHeight || elementPoint < scrollTopPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an element is visible or not.
|
||||||
|
*
|
||||||
|
* @param element Element.
|
||||||
|
*/
|
||||||
|
isElementVisible(element: HTMLElement): boolean {
|
||||||
|
if (element.clientWidth === 0 || element.clientHeight === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
if (style.opacity === '0' || style.display === 'none' || style.visibility === 'hidden') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.offsetParent !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if rich text editor is enabled.
|
* Check if rich text editor is enabled.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1765,34 +1765,6 @@ export class CoreUtilsProvider {
|
||||||
return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH;
|
return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the given cookie is set.
|
|
||||||
*
|
|
||||||
* @param name Cookie name.
|
|
||||||
* @returns Whether the cookie is set.
|
|
||||||
*/
|
|
||||||
hasCookie(name: string): boolean {
|
|
||||||
return new RegExp(`(\\s|;|^)${name}=`).test(document.cookie ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a cookie.
|
|
||||||
*
|
|
||||||
* @param name Cookie name.
|
|
||||||
* @return Cookie value.
|
|
||||||
*/
|
|
||||||
getCookie(name: string): string | null {
|
|
||||||
const cookies = (document.cookie ?? '').split(';').reduce((cookies, cookie) => {
|
|
||||||
const [name, value] = cookie.trim().split('=');
|
|
||||||
|
|
||||||
cookies[name] = value;
|
|
||||||
|
|
||||||
return cookies;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return cookies[name] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUtils = makeSingleton(CoreUtilsProvider);
|
export const CoreUtils = makeSingleton(CoreUtilsProvider);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers to interact with Browser APIs.
|
||||||
|
*/
|
||||||
|
export class CoreBrowser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the given cookie is set.
|
||||||
|
*
|
||||||
|
* @param name Cookie name.
|
||||||
|
* @returns Whether the cookie is set.
|
||||||
|
*/
|
||||||
|
static hasCookie(name: string): boolean {
|
||||||
|
return new RegExp(`(\\s|;|^)${name}=`).test(document.cookie ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a cookie.
|
||||||
|
*
|
||||||
|
* @param name Cookie name.
|
||||||
|
* @return Cookie value.
|
||||||
|
*/
|
||||||
|
static getCookie(name: string): string | null {
|
||||||
|
const cookies = (document.cookie ?? '').split(';').reduce((cookies, cookie) => {
|
||||||
|
const [name, value] = cookie.trim().split('=');
|
||||||
|
|
||||||
|
cookies[name] = value;
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return cookies[name] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ import moment from 'moment';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
|
||||||
import { CoreTime } from './time';
|
import { CoreTime } from './time';
|
||||||
|
import { CoreBrowser } from '@singletons/browser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to warn that logs are disabled, called only once.
|
* Method to warn that logs are disabled, called only once.
|
||||||
|
@ -67,7 +68,10 @@ export class CoreLogger {
|
||||||
*/
|
*/
|
||||||
static getInstance(className: string): CoreLogger {
|
static getInstance(className: string): CoreLogger {
|
||||||
// Disable log on production and testing.
|
// Disable log on production and testing.
|
||||||
if (CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting) {
|
if (
|
||||||
|
!CoreBrowser.hasCookie('MoodleAppLoggingEnabled') &&
|
||||||
|
(CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting)
|
||||||
|
) {
|
||||||
if (CoreConstants.BUILD.isProduction) {
|
if (CoreConstants.BUILD.isProduction) {
|
||||||
warnLogsDisabled();
|
warnLogsDisabled();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`;
|
||||||
|
}, '');
|
||||||
|
}
|
|
@ -34,6 +34,8 @@ export interface EnvironmentConfig {
|
||||||
languages: Record<string, string>;
|
languages: Record<string, string>;
|
||||||
databaseOptimizations?: Partial<CoreDatabaseConfiguration>;
|
databaseOptimizations?: Partial<CoreDatabaseConfiguration>;
|
||||||
databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>;
|
databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>;
|
||||||
|
disableUserTours?: boolean;
|
||||||
|
disabledUserTours?: string[];
|
||||||
wsservice: string;
|
wsservice: string;
|
||||||
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
||||||
zoomlevels: Record<CoreZoomLevel, number>;
|
zoomlevels: Record<CoreZoomLevel, number>;
|
||||||
|
|
Loading…
Reference in New Issue