forked from EVOgeek/Vmeda.Online
		
	Merge pull request #3164 from NoelDeMartin/MOBILE-3153
MOBILE-3153: User Tours
This commit is contained in:
		
						commit
						1db5b85e40
					
				| @ -139,7 +139,6 @@ const appConfig = { | ||||
|             'always', | ||||
|         ], | ||||
|         '@typescript-eslint/type-annotation-spacing': 'error', | ||||
|         '@typescript-eslint/unified-signatures': 'error', | ||||
|         'header/header': [ | ||||
|             2, | ||||
|             '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('}', '}}', $text); | ||||
|             $text = preg_replace('/@@.+?@@(<br>)?\\s*/', '', $text); | ||||
|             // Prevent double.
 | ||||
|             $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); | ||||
|         } else { | ||||
|  | ||||
| @ -1450,6 +1450,8 @@ | ||||
|   "core.block.blocks": "moodle", | ||||
|   "core.block.noblocks": "error", | ||||
|   "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.cancel": "moodle", | ||||
|   "core.cannotconnect": "local_moodlemobileapp", | ||||
| @ -1567,6 +1569,8 @@ | ||||
|   "core.course.startdate": "moodle", | ||||
|   "core.course.thisweek": "format_weeks/currentsection", | ||||
|   "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.viewcourse": "block_timeline", | ||||
|   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", | ||||
| @ -2003,6 +2007,8 @@ | ||||
|   "core.mainmenu.home": "moodle", | ||||
|   "core.mainmenu.logout": "moodle", | ||||
|   "core.mainmenu.switchaccount": "local_moodlemobileapp", | ||||
|   "core.mainmenu.usermenutourdescription": "local_moodlemobileapp", | ||||
|   "core.mainmenu.usermenutourtitle": "local_moodlemobileapp", | ||||
|   "core.maxfilesize": "moodle", | ||||
|   "core.maxsizeandattachments": "moodle", | ||||
|   "core.min": "moodle", | ||||
| @ -2263,6 +2269,7 @@ | ||||
|   "core.submit": "moodle", | ||||
|   "core.success": "moodle", | ||||
|   "core.summary": "moodle", | ||||
|   "core.swipenavigationtourdescription": "local_moodlemobileapp", | ||||
|   "core.tablet": "local_moodlemobileapp", | ||||
|   "core.tag.defautltagcoll": "tag", | ||||
|   "core.tag.errorareanotsupported": "local_moodlemobileapp", | ||||
| @ -2335,6 +2342,7 @@ | ||||
|   "core.usernotfullysetup": "error", | ||||
|   "core.users": "moodle", | ||||
|   "core.usersuspended": "tool_reportbuilder", | ||||
|   "core.endonesteptour": "tool_usertours", | ||||
|   "core.view": "moodle", | ||||
|   "core.viewcode": "local_moodlemobileapp", | ||||
|   "core.vieweditor": "local_moodlemobileapp", | ||||
|  | ||||
							
								
								
									
										1
									
								
								src/assets/img/user-tours/course-index.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/img/user-tours/course-index.svg
									
									
									
									
									
										Normal file
									
								
							| @ -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 | 
							
								
								
									
										1
									
								
								src/assets/img/user-tours/side-blocks.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/img/user-tours/side-blocks.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										1
									
								
								src/assets/img/user-tours/swipe-navigation.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/img/user-tours/swipe-navigation.svg
									
									
									
									
									
										Normal file
									
								
							| @ -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 | 
							
								
								
									
										1
									
								
								src/assets/img/user-tours/user-menu.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/img/user-tours/user-menu.svg
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||
|         return this.target.hasAnyByPrimaryKey(primaryKey); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @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. | ||||
|      * | ||||
|  | ||||
| @ -129,6 +129,15 @@ export class CoreDebugDatabaseTable< | ||||
|         return this.target.hasAny(conditions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||
|         this.logger.log('hasAnyByPrimaryKey', primaryKey); | ||||
| 
 | ||||
|         return this.target.hasAnyByPrimaryKey(primaryKey); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|  | ||||
| @ -134,6 +134,13 @@ export class CoreEagerDatabaseTable< | ||||
|             : Object.values(this.records).length > 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||
|         return this.serializePrimaryKey(primaryKey) in this.records; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|  | ||||
| @ -125,6 +125,15 @@ export class CoreLazyDatabaseTable< | ||||
|         return super.hasAny(conditions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||
|         const record = this.records[this.serializePrimaryKey(primaryKey)] ?? null; | ||||
| 
 | ||||
|         return record !== null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|  | ||||
| @ -60,6 +60,7 @@ import { CoreSpacerComponent } from './spacer/spacer'; | ||||
| import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls'; | ||||
| import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner'; | ||||
| import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | ||||
| import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -102,6 +103,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | ||||
|         CoreComboboxComponent, | ||||
|         CoreSpacerComponent, | ||||
|         CoreHorizontalScrollControlsComponent, | ||||
|         CoreSwipeNavigationTourComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
| @ -151,6 +153,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | ||||
|         CoreComboboxComponent, | ||||
|         CoreSpacerComponent, | ||||
|         CoreHorizontalScrollControlsComponent, | ||||
|         CoreSwipeNavigationTourComponent, | ||||
|     ], | ||||
| }) | ||||
| 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)); | ||||
| 
 | ||||
|             onOutletUpdated(); | ||||
| 
 | ||||
|             onOutletUpdated(); | ||||
| 
 | ||||
| @ -311,6 +314,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | ||||
|      * @param content Content element. | ||||
|      */ | ||||
|     protected updateContent(content?: HTMLIonContentElement | null): void { | ||||
|         if (content === (this.content ?? null)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.content) { | ||||
|             if (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); | ||||
|                 delete this.endContentScrollListener; | ||||
|             } | ||||
| 
 | ||||
|             delete this.content; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -32,6 +32,7 @@ import { CoreSwipeNavigationDirective } from './swipe-navigation'; | ||||
| import { CoreCollapsibleItemDirective } from './collapsible-item'; | ||||
| import { CoreCollapsibleFooterDirective } from './collapsible-footer'; | ||||
| import { CoreContentDirective } from './content'; | ||||
| import { CoreOnAppearDirective } from './on-appear'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -46,6 +47,7 @@ import { CoreContentDirective } from './content'; | ||||
|         CoreSupressEventsDirective, | ||||
|         CoreUserLinkDirective, | ||||
|         CoreAriaButtonClickDirective, | ||||
|         CoreOnAppearDirective, | ||||
|         CoreOnResizeDirective, | ||||
|         CoreDownloadFileDirective, | ||||
|         CoreCollapsibleHeaderDirective, | ||||
| @ -66,6 +68,7 @@ import { CoreContentDirective } from './content'; | ||||
|         CoreSupressEventsDirective, | ||||
|         CoreUserLinkDirective, | ||||
|         CoreAriaButtonClickDirective, | ||||
|         CoreOnAppearDirective, | ||||
|         CoreOnResizeDirective, | ||||
|         CoreDownloadFileDirective, | ||||
|         CoreCollapsibleHeaderDirective, | ||||
|  | ||||
							
								
								
									
										58
									
								
								src/core/directives/on-appear.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/core/directives/on-appear.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { 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 { CoreScreen } from '@services/screen'; | ||||
| import { GestureController, Platform } from '@singletons'; | ||||
| @ -50,7 +52,8 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngAfterViewInit(): void { | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         // Set up gesture listener
 | ||||
|         const style = this.element.style; | ||||
|         this.swipeGesture = GestureController.create({ | ||||
|             el: this.element, | ||||
| @ -71,6 +74,26 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|             }, | ||||
|         }); | ||||
|         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 { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks'; | ||||
| import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button'; | ||||
| import { CoreBlockSideBlocksTourComponent } from './side-blocks-tour/side-blocks-tour'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -27,6 +28,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl | ||||
|         CoreBlockPreRenderedComponent, | ||||
|         CoreBlockSideBlocksComponent, | ||||
|         CoreBlockSideBlocksButtonComponent, | ||||
|         CoreBlockSideBlocksTourComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
| @ -37,6 +39,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl | ||||
|         CoreBlockPreRenderedComponent, | ||||
|         CoreBlockSideBlocksComponent, | ||||
|         CoreBlockSideBlocksButtonComponent, | ||||
|         CoreBlockSideBlocksTourComponent, | ||||
|     ], | ||||
| }) | ||||
| 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-button> | ||||
|  | ||||
| @ -12,8 +12,10 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // 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 { CoreBlockSideBlocksTourComponent } from '../side-blocks-tour/side-blocks-tour'; | ||||
| import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks'; | ||||
| 
 | ||||
| /** | ||||
| @ -27,6 +29,7 @@ import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks'; | ||||
| export class CoreBlockSideBlocksButtonComponent { | ||||
| 
 | ||||
|     @Input() courseId!: number; | ||||
|     @ViewChild('button', { read: ElementRef }) button?: ElementRef<HTMLElement>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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", | ||||
|     "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 { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; | ||||
| import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary'; | ||||
| import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -39,6 +40,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar | ||||
|         CoreCourseModuleInfoComponent, | ||||
|         CoreCourseModuleManualCompletionComponent, | ||||
|         CoreCourseCourseIndexComponent, | ||||
|         CoreCourseCourseIndexTourComponent, | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
| @ -57,6 +59,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar | ||||
|         CoreCourseModuleInfoComponent, | ||||
|         CoreCourseModuleManualCompletionComponent, | ||||
|         CoreCourseCourseIndexComponent, | ||||
|         CoreCourseCourseIndexTourComponent, | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
|  | ||||
| @ -49,7 +49,8 @@ | ||||
| 
 | ||||
| <!-- Course Index button. --> | ||||
| <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> | ||||
|         <span class="sr-only">{{'core.course.courseindex' | translate }}</span> | ||||
|     </ion-fab-button> | ||||
|  | ||||
| @ -23,6 +23,7 @@ import { | ||||
|     QueryList, | ||||
|     Type, | ||||
|     ElementRef, | ||||
|     ViewChild, | ||||
| } from '@angular/core'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 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 { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| 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. | ||||
| @ -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.
 | ||||
| 
 | ||||
|     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; | ||||
|     @ViewChild('courseIndexFab', { read: ElementRef }) courseIndexFab?: ElementRef<HTMLElement>; | ||||
| 
 | ||||
|     // All the possible component classes.
 | ||||
|     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. | ||||
|      */ | ||||
|  | ||||
| @ -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", | ||||
|     "thisweek": "This week", | ||||
|     "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.", | ||||
|     "viewcourse": "View course", | ||||
|     "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 { CoreStylesModule } from './styles/styles.module'; | ||||
| import { CoreTagModule } from './tag/tag.module'; | ||||
| import { CoreUserToursModule } from './usertours/user-tours.module'; | ||||
| import { CoreUserModule } from './user/user.module'; | ||||
| import { CoreViewerModule } from './viewer/viewer.module'; | ||||
| import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
| @ -66,6 +67,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
|         CoreSitePluginsModule, | ||||
|         CoreTagModule, | ||||
|         CoreStylesModule, | ||||
|         CoreUserToursModule, | ||||
|         CoreUserModule, | ||||
|         CoreViewerModule, | ||||
|         CoreXAPIModule, | ||||
|  | ||||
| @ -17,11 +17,13 @@ import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; | ||||
| import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; | ||||
| import { CoreLoginComponentsModule } from '@features/login/components/components.module'; | ||||
| import { CoreMainMenuUserMenuTourComponent } from './user-menu-tour/user-menu-tour'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreMainMenuUserButtonComponent, | ||||
|         CoreMainMenuUserMenuComponent, | ||||
|         CoreMainMenuUserMenuTourComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
| @ -30,6 +32,7 @@ import { CoreLoginComponentsModule } from '@features/login/components/components | ||||
|     exports: [ | ||||
|         CoreMainMenuUserButtonComponent, | ||||
|         CoreMainMenuUserMenuComponent, | ||||
|         CoreMainMenuUserMenuTourComponent, | ||||
|     ], | ||||
| }) | ||||
| export class CoreMainMenuComponentsModule {} | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| <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> | ||||
|  | ||||
| @ -12,11 +12,13 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; | ||||
| import { CoreSiteInfo } from '@classes/site'; | ||||
| import { CoreUserTours, CoreUserToursStyle } from '@features/usertours/services/user-tours'; | ||||
| import { IonRouterOutlet } from '@ionic/angular'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreMainMenuUserMenuTourComponent } from '../user-menu-tour/user-menu-tour'; | ||||
| import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu'; | ||||
| 
 | ||||
| /** | ||||
| @ -34,6 +36,8 @@ export class CoreMainMenuUserButtonComponent implements OnInit { | ||||
|     siteInfo?: CoreSiteInfo; | ||||
|     isMainScreen = false; | ||||
| 
 | ||||
|     @ViewChild('avatar', { read: ElementRef }) avatar?: ElementRef<HTMLElement>; | ||||
| 
 | ||||
|     constructor(protected routerOutlet: IonRouterOutlet) { | ||||
|         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", | ||||
|     "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" | ||||
| } | ||||
|  | ||||
							
								
								
									
										50
									
								
								src/core/features/usertours/classes/focus-layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/core/features/usertours/classes/focus-layout.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										213
									
								
								src/core/features/usertours/classes/popover-layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/core/features/usertours/classes/popover-layout.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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%)'; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/core/features/usertours/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/core/features/usertours/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
| } | ||||
							
								
								
									
										168
									
								
								src/core/features/usertours/components/user-tour/user-tour.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/core/features/usertours/components/user-tour/user-tour.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/core/features/usertours/services/database/user-tours.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/core/features/usertours/services/database/user-tours.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| }; | ||||
							
								
								
									
										262
									
								
								src/core/features/usertours/services/user-tours.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/core/features/usertours/services/user-tours.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										37
									
								
								src/core/features/usertours/user-tours.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/core/features/usertours/user-tours.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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", | ||||
|     "success": "Success", | ||||
|     "summary": "Summary", | ||||
|     "swipenavigationtourdescription": "Swipe left and right to navigate around.", | ||||
|     "tablet": "Tablet", | ||||
|     "teachers": "Teachers", | ||||
|     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", | ||||
| @ -329,6 +330,7 @@ | ||||
|     "usernotfullysetup": "User not fully set-up", | ||||
|     "usernologin": "Authentication has been revoked for this account", | ||||
|     "usersuspended": "Registration suspended", | ||||
|     "endonesteptour": "Got it", | ||||
|     "users": "Users", | ||||
|     "view": "View", | ||||
|     "viewcode": "View code", | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreDatabaseTable } from '@classes/database/database-table'; | ||||
| import { asyncInstance } from '../utils/async-instance'; | ||||
| import { CorePromisedValue } from '@classes/promised-value'; | ||||
| import { CoreUtils } from './utils/utils'; | ||||
| import { CoreBrowser } from '@singletons/browser'; | ||||
| 
 | ||||
| declare module '@singletons/events' { | ||||
| 
 | ||||
| @ -195,11 +195,11 @@ export class CoreConfigProvider { | ||||
|      * Load development config overrides. | ||||
|      */ | ||||
|     protected loadDevelopmentConfig(): void { | ||||
|         if (!CoreConstants.enableDevTools() || !CoreUtils.hasCookie('MoodleAppConfig')) { | ||||
|         if (!CoreConstants.enableDevTools() || !CoreBrowser.hasCookie('MoodleAppConfig')) { | ||||
|             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 { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; | ||||
| import { CoreBrowser } from '@singletons/browser'; | ||||
| import { makeSingleton, SQLite, Platform } from '@singletons'; | ||||
| import { CoreAppProvider } from './app'; | ||||
| import { CoreUtils } from './utils/utils'; | ||||
| 
 | ||||
| /** | ||||
|  * 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. | ||||
|      */ | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
|  | ||||
| @ -1765,34 +1765,6 @@ export class CoreUtilsProvider { | ||||
|         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); | ||||
|  | ||||
							
								
								
									
										48
									
								
								src/core/singletons/browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/core/singletons/browser.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
|  | ||||
| @ -12,13 +12,22 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // 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 { HttpClient } from '@angular/common/http'; | ||||
| import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser'; | ||||
| 
 | ||||
| import { | ||||
|     Platform as PlatformService, | ||||
|     AngularDelegate as AngularDelegateService, | ||||
|     AlertController as AlertControllerService, | ||||
|     LoadingController as LoadingControllerService, | ||||
|     ModalController as ModalControllerService, | ||||
| @ -58,11 +67,13 @@ import { Zip as ZipService } from '@ionic-native/zip/ngx'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| 
 | ||||
| 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. | ||||
|  */ | ||||
| let singletonsInjector: Injector | null = null; | ||||
| const singletonsInjector = new CorePromisedValue<Injector>(); | ||||
| 
 | ||||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| 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', { | ||||
|         get: () => { | ||||
|             if (!singletonsInjector) { | ||||
|             const injector = singletonsInjector.value; | ||||
| 
 | ||||
|             if (!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); | ||||
| 
 | ||||
| @ -194,7 +207,9 @@ export const NgZone = makeSingleton(NgZoneService); | ||||
| export const Http = makeSingleton(HttpClient); | ||||
| export const Platform = makeSingleton(PlatformService); | ||||
| export const ActionSheetController = makeSingleton(ActionSheetControllerService); | ||||
| export const AngularDelegate = makeSingleton(AngularDelegateService); | ||||
| export const AlertController = makeSingleton(AlertControllerService); | ||||
| export const ComponentFactoryResolver = makeSingleton(ComponentFactoryResolverService); | ||||
| export const LoadingController = makeSingleton(LoadingControllerService); | ||||
| export const ModalController = makeSingleton(ModalControllerService); | ||||
| export const PopoverController = makeSingleton(PopoverControllerService); | ||||
| @ -208,3 +223,10 @@ export const DomSanitizer = makeSingleton(DomSanitizerService); | ||||
| 
 | ||||
| // Convert external libraries injectables.
 | ||||
| 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 { CoreTime } from './time'; | ||||
| import { CoreBrowser } from '@singletons/browser'; | ||||
| 
 | ||||
| /** | ||||
|  * Method to warn that logs are disabled, called only once. | ||||
| @ -67,7 +68,10 @@ export class CoreLogger { | ||||
|      */ | ||||
|     static getInstance(className: string): CoreLogger { | ||||
|         // 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) { | ||||
|                 warnLogsDisabled(); | ||||
|             } | ||||
|  | ||||
| @ -54,6 +54,20 @@ function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>): | ||||
| 
 | ||||
|             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() { | ||||
|             if (!promisedInstance) { | ||||
|                 return; | ||||
| @ -72,6 +86,7 @@ export interface AsyncInstanceWrapper<T> { | ||||
|     getInstance(): Promise<T>; | ||||
|     getProperty<P extends keyof T>(property: P): Promise<T[P]>; | ||||
|     setInstance(instance: T): void; | ||||
|     setLazyConstructor(lazyConstructor: () => T | Promise<T>): void; | ||||
|     resetInstance(): void; | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										36
									
								
								src/core/utils/style-helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/core/utils/style-helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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}`; | ||||
|         }, ''); | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -34,6 +34,8 @@ export interface EnvironmentConfig { | ||||
|     languages: Record<string, string>; | ||||
|     databaseOptimizations?: Partial<CoreDatabaseConfiguration>; | ||||
|     databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>; | ||||
|     disableUserTours?: boolean; | ||||
|     disabledUserTours?: string[]; | ||||
|     wsservice: string; | ||||
|     demo_sites: Record<string, CoreSitesDemoSiteData>; | ||||
|     zoomlevels: Record<CoreZoomLevel, number>; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user