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', |             'always', | ||||||
|         ], |         ], | ||||||
|         '@typescript-eslint/type-annotation-spacing': 'error', |         '@typescript-eslint/type-annotation-spacing': 'error', | ||||||
|         '@typescript-eslint/unified-signatures': 'error', |  | ||||||
|         'header/header': [ |         'header/header': [ | ||||||
|             2, |             2, | ||||||
|             'line', |             'line', | ||||||
|  | |||||||
| @ -257,6 +257,7 @@ function build_lang($lang, $keys) { | |||||||
|             $text = str_replace('$a->', '$a.', $text); |             $text = str_replace('$a->', '$a.', $text); | ||||||
|             $text = str_replace('{$a', '{{$a', $text); |             $text = str_replace('{$a', '{{$a', $text); | ||||||
|             $text = str_replace('}', '}}', $text); |             $text = str_replace('}', '}}', $text); | ||||||
|  |             $text = preg_replace('/@@.+?@@(<br>)?\\s*/', '', $text); | ||||||
|             // Prevent double.
 |             // Prevent double.
 | ||||||
|             $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); |             $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); | ||||||
|         } else { |         } else { | ||||||
|  | |||||||
| @ -1450,6 +1450,8 @@ | |||||||
|   "core.block.blocks": "moodle", |   "core.block.blocks": "moodle", | ||||||
|   "core.block.noblocks": "error", |   "core.block.noblocks": "error", | ||||||
|   "core.block.opendrawerblocks": "moodle", |   "core.block.opendrawerblocks": "moodle", | ||||||
|  |   "core.block.tour_navigation_dashboard_content": "tool_usertours", | ||||||
|  |   "core.block.tour_navigation_dashboard_title": "tool_usertours", | ||||||
|   "core.browser": "local_moodlemobileapp", |   "core.browser": "local_moodlemobileapp", | ||||||
|   "core.cancel": "moodle", |   "core.cancel": "moodle", | ||||||
|   "core.cannotconnect": "local_moodlemobileapp", |   "core.cannotconnect": "local_moodlemobileapp", | ||||||
| @ -1567,6 +1569,8 @@ | |||||||
|   "core.course.startdate": "moodle", |   "core.course.startdate": "moodle", | ||||||
|   "core.course.thisweek": "format_weeks/currentsection", |   "core.course.thisweek": "format_weeks/currentsection", | ||||||
|   "core.course.todo": "completion", |   "core.course.todo": "completion", | ||||||
|  |   "core.course.tour_navigation_course_index_student_content": "tool_usertours", | ||||||
|  |   "core.course.tour_navigation_course_index_student_title": "tool_usertours", | ||||||
|   "core.course.useactivityonbrowser": "local_moodlemobileapp", |   "core.course.useactivityonbrowser": "local_moodlemobileapp", | ||||||
|   "core.course.viewcourse": "block_timeline", |   "core.course.viewcourse": "block_timeline", | ||||||
|   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", |   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", | ||||||
| @ -2003,6 +2007,8 @@ | |||||||
|   "core.mainmenu.home": "moodle", |   "core.mainmenu.home": "moodle", | ||||||
|   "core.mainmenu.logout": "moodle", |   "core.mainmenu.logout": "moodle", | ||||||
|   "core.mainmenu.switchaccount": "local_moodlemobileapp", |   "core.mainmenu.switchaccount": "local_moodlemobileapp", | ||||||
|  |   "core.mainmenu.usermenutourdescription": "local_moodlemobileapp", | ||||||
|  |   "core.mainmenu.usermenutourtitle": "local_moodlemobileapp", | ||||||
|   "core.maxfilesize": "moodle", |   "core.maxfilesize": "moodle", | ||||||
|   "core.maxsizeandattachments": "moodle", |   "core.maxsizeandattachments": "moodle", | ||||||
|   "core.min": "moodle", |   "core.min": "moodle", | ||||||
| @ -2263,6 +2269,7 @@ | |||||||
|   "core.submit": "moodle", |   "core.submit": "moodle", | ||||||
|   "core.success": "moodle", |   "core.success": "moodle", | ||||||
|   "core.summary": "moodle", |   "core.summary": "moodle", | ||||||
|  |   "core.swipenavigationtourdescription": "local_moodlemobileapp", | ||||||
|   "core.tablet": "local_moodlemobileapp", |   "core.tablet": "local_moodlemobileapp", | ||||||
|   "core.tag.defautltagcoll": "tag", |   "core.tag.defautltagcoll": "tag", | ||||||
|   "core.tag.errorareanotsupported": "local_moodlemobileapp", |   "core.tag.errorareanotsupported": "local_moodlemobileapp", | ||||||
| @ -2335,6 +2342,7 @@ | |||||||
|   "core.usernotfullysetup": "error", |   "core.usernotfullysetup": "error", | ||||||
|   "core.users": "moodle", |   "core.users": "moodle", | ||||||
|   "core.usersuspended": "tool_reportbuilder", |   "core.usersuspended": "tool_reportbuilder", | ||||||
|  |   "core.endonesteptour": "tool_usertours", | ||||||
|   "core.view": "moodle", |   "core.view": "moodle", | ||||||
|   "core.viewcode": "local_moodlemobileapp", |   "core.viewcode": "local_moodlemobileapp", | ||||||
|   "core.vieweditor": "local_moodlemobileapp", |   "core.vieweditor": "local_moodlemobileapp", | ||||||
|  | |||||||
							
								
								
									
										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); |         return this.target.hasAny(conditions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||||
|  |         return this.target.hasAnyByPrimaryKey(primaryKey); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -219,6 +219,23 @@ export class CoreDatabaseTable< | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether the table has any record matching the given primary key. | ||||||
|  |      * | ||||||
|  |      * @param primaryKey Record primary key. | ||||||
|  |      * @returns Whether the table contains a record matching the given primary key. | ||||||
|  |      */ | ||||||
|  |     async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||||
|  |         try { | ||||||
|  |             await this.getOneByPrimaryKey(primaryKey); | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         } catch (error) { | ||||||
|  |             // Couldn't get the record.
 | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Count records in table. |      * Count records in table. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -129,6 +129,15 @@ export class CoreDebugDatabaseTable< | |||||||
|         return this.target.hasAny(conditions); |         return this.target.hasAny(conditions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||||
|  |         this.logger.log('hasAnyByPrimaryKey', primaryKey); | ||||||
|  | 
 | ||||||
|  |         return this.target.hasAnyByPrimaryKey(primaryKey); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -134,6 +134,13 @@ export class CoreEagerDatabaseTable< | |||||||
|             : Object.values(this.records).length > 0; |             : Object.values(this.records).length > 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||||
|  |         return this.serializePrimaryKey(primaryKey) in this.records; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -125,6 +125,15 @@ export class CoreLazyDatabaseTable< | |||||||
|         return super.hasAny(conditions); |         return super.hasAny(conditions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async hasAnyByPrimaryKey(primaryKey: PrimaryKey): Promise<boolean> { | ||||||
|  |         const record = this.records[this.serializePrimaryKey(primaryKey)] ?? null; | ||||||
|  | 
 | ||||||
|  |         return record !== null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -60,6 +60,7 @@ import { CoreSpacerComponent } from './spacer/spacer'; | |||||||
| import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls'; | import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls'; | ||||||
| import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner'; | import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner'; | ||||||
| import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | ||||||
|  | import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -102,6 +103,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | |||||||
|         CoreComboboxComponent, |         CoreComboboxComponent, | ||||||
|         CoreSpacerComponent, |         CoreSpacerComponent, | ||||||
|         CoreHorizontalScrollControlsComponent, |         CoreHorizontalScrollControlsComponent, | ||||||
|  |         CoreSwipeNavigationTourComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CommonModule, |         CommonModule, | ||||||
| @ -151,6 +153,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; | |||||||
|         CoreComboboxComponent, |         CoreComboboxComponent, | ||||||
|         CoreSpacerComponent, |         CoreSpacerComponent, | ||||||
|         CoreHorizontalScrollControlsComponent, |         CoreHorizontalScrollControlsComponent, | ||||||
|  |         CoreSwipeNavigationTourComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreComponentsModule {} | export class CoreComponentsModule {} | ||||||
|  | |||||||
| @ -0,0 +1,5 @@ | |||||||
|  | <img src="assets/img/user-tours/swipe-navigation.svg" alt="" /> | ||||||
|  | <p>{{ 'core.swipenavigationtourdescription' | translate }}</p> | ||||||
|  | <ion-button (click)="dismiss()" expand="block"> | ||||||
|  |     {{ 'core.endonesteptour' | translate }} | ||||||
|  | </ion-button> | ||||||
| @ -0,0 +1,12 @@ | |||||||
|  | :host { | ||||||
|  |     max-width: 85vw; | ||||||
|  | 
 | ||||||
|  |     img { | ||||||
|  |         max-width: 300px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     p { | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { CoreUserTours } from '@features/usertours/services/user-tours'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component showing the User Tour for the Swipe Navigation feature. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-swipe-navigation-tour', | ||||||
|  |     templateUrl: 'core-swipe-navigation-tour.html', | ||||||
|  |     styleUrls: ['swipe-navigation-tour.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreSwipeNavigationTourComponent { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Dismiss User Tour. | ||||||
|  |      */ | ||||||
|  |     async dismiss(): Promise<void> { | ||||||
|  |         await CoreUserTours.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -190,6 +190,9 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | |||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated)); |             this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated)); | ||||||
|  |             this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated)); | ||||||
|  | 
 | ||||||
|  |             onOutletUpdated(); | ||||||
| 
 | 
 | ||||||
|             onOutletUpdated(); |             onOutletUpdated(); | ||||||
| 
 | 
 | ||||||
| @ -311,6 +314,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | |||||||
|      * @param content Content element. |      * @param content Content element. | ||||||
|      */ |      */ | ||||||
|     protected updateContent(content?: HTMLIonContentElement | null): void { |     protected updateContent(content?: HTMLIonContentElement | null): void { | ||||||
|  |         if (content === (this.content ?? null)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (this.content) { |         if (this.content) { | ||||||
|             if (this.contentScrollListener) { |             if (this.contentScrollListener) { | ||||||
|                 this.content.removeEventListener('ionScroll', this.contentScrollListener); |                 this.content.removeEventListener('ionScroll', this.contentScrollListener); | ||||||
| @ -321,6 +328,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | |||||||
|                 this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener); |                 this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener); | ||||||
|                 delete this.endContentScrollListener; |                 delete this.endContentScrollListener; | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|             delete this.content; |             delete this.content; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ import { CoreSwipeNavigationDirective } from './swipe-navigation'; | |||||||
| import { CoreCollapsibleItemDirective } from './collapsible-item'; | import { CoreCollapsibleItemDirective } from './collapsible-item'; | ||||||
| import { CoreCollapsibleFooterDirective } from './collapsible-footer'; | import { CoreCollapsibleFooterDirective } from './collapsible-footer'; | ||||||
| import { CoreContentDirective } from './content'; | import { CoreContentDirective } from './content'; | ||||||
|  | import { CoreOnAppearDirective } from './on-appear'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -46,6 +47,7 @@ import { CoreContentDirective } from './content'; | |||||||
|         CoreSupressEventsDirective, |         CoreSupressEventsDirective, | ||||||
|         CoreUserLinkDirective, |         CoreUserLinkDirective, | ||||||
|         CoreAriaButtonClickDirective, |         CoreAriaButtonClickDirective, | ||||||
|  |         CoreOnAppearDirective, | ||||||
|         CoreOnResizeDirective, |         CoreOnResizeDirective, | ||||||
|         CoreDownloadFileDirective, |         CoreDownloadFileDirective, | ||||||
|         CoreCollapsibleHeaderDirective, |         CoreCollapsibleHeaderDirective, | ||||||
| @ -66,6 +68,7 @@ import { CoreContentDirective } from './content'; | |||||||
|         CoreSupressEventsDirective, |         CoreSupressEventsDirective, | ||||||
|         CoreUserLinkDirective, |         CoreUserLinkDirective, | ||||||
|         CoreAriaButtonClickDirective, |         CoreAriaButtonClickDirective, | ||||||
|  |         CoreOnAppearDirective, | ||||||
|         CoreOnResizeDirective, |         CoreOnResizeDirective, | ||||||
|         CoreDownloadFileDirective, |         CoreDownloadFileDirective, | ||||||
|         CoreCollapsibleHeaderDirective, |         CoreCollapsibleHeaderDirective, | ||||||
|  | |||||||
							
								
								
									
										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 {  AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core'; | ||||||
| import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | ||||||
|  | import { CoreSwipeNavigationTourComponent } from '@components/swipe-navigation-tour/swipe-navigation-tour'; | ||||||
|  | import { CoreUserTours } from '@features/usertours/services/user-tours'; | ||||||
| import { Gesture, GestureDetail } from '@ionic/angular'; | import { Gesture, GestureDetail } from '@ionic/angular'; | ||||||
| import { CoreScreen } from '@services/screen'; | import { CoreScreen } from '@services/screen'; | ||||||
| import { GestureController, Platform } from '@singletons'; | import { GestureController, Platform } from '@singletons'; | ||||||
| @ -50,7 +52,8 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngAfterViewInit(): void { |     async ngAfterViewInit(): Promise<void> { | ||||||
|  |         // Set up gesture listener
 | ||||||
|         const style = this.element.style; |         const style = this.element.style; | ||||||
|         this.swipeGesture = GestureController.create({ |         this.swipeGesture = GestureController.create({ | ||||||
|             el: this.element, |             el: this.element, | ||||||
| @ -71,6 +74,26 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | |||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
|         this.swipeGesture.enable(); |         this.swipeGesture.enable(); | ||||||
|  | 
 | ||||||
|  |         // Show user tour.
 | ||||||
|  |         const source = this.manager?.getSource(); | ||||||
|  | 
 | ||||||
|  |         if (!source) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await source.waitForLoaded(); | ||||||
|  | 
 | ||||||
|  |         const items = source.getItems() ?? []; | ||||||
|  | 
 | ||||||
|  |         if (items.length < 2) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await CoreUserTours.showIfPending({ | ||||||
|  |             id: 'swipe-navigation', | ||||||
|  |             component: CoreSwipeNavigationTourComponent, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered | |||||||
| import { CoreSharedModule } from '@/core/shared.module'; | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
| import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks'; | import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks'; | ||||||
| import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button'; | import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button'; | ||||||
|  | import { CoreBlockSideBlocksTourComponent } from './side-blocks-tour/side-blocks-tour'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -27,6 +28,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl | |||||||
|         CoreBlockPreRenderedComponent, |         CoreBlockPreRenderedComponent, | ||||||
|         CoreBlockSideBlocksComponent, |         CoreBlockSideBlocksComponent, | ||||||
|         CoreBlockSideBlocksButtonComponent, |         CoreBlockSideBlocksButtonComponent, | ||||||
|  |         CoreBlockSideBlocksTourComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CoreSharedModule, |         CoreSharedModule, | ||||||
| @ -37,6 +39,7 @@ import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-bl | |||||||
|         CoreBlockPreRenderedComponent, |         CoreBlockPreRenderedComponent, | ||||||
|         CoreBlockSideBlocksComponent, |         CoreBlockSideBlocksComponent, | ||||||
|         CoreBlockSideBlocksButtonComponent, |         CoreBlockSideBlocksButtonComponent, | ||||||
|  |         CoreBlockSideBlocksTourComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreBlockComponentsModule {} | export class CoreBlockComponentsModule {} | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
| <ion-button (click)="openBlocks()" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary"> | <ion-button (click)="openBlocks()" (onAppear)="showTour()" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary" | ||||||
|  |     #button> | ||||||
|     <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> |     <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
| </ion-button> | </ion-button> | ||||||
|  | |||||||
| @ -12,8 +12,10 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, Input } from '@angular/core'; | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; | ||||||
|  | import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreBlockSideBlocksTourComponent } from '../side-blocks-tour/side-blocks-tour'; | ||||||
| import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks'; | import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -27,6 +29,7 @@ import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks'; | |||||||
| export class CoreBlockSideBlocksButtonComponent { | export class CoreBlockSideBlocksButtonComponent { | ||||||
| 
 | 
 | ||||||
|     @Input() courseId!: number; |     @Input() courseId!: number; | ||||||
|  |     @ViewChild('button', { read: ElementRef }) button?: ElementRef<HTMLElement>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Open side blocks. |      * Open side blocks. | ||||||
| @ -40,4 +43,23 @@ export class CoreBlockSideBlocksButtonComponent { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Show User Tour. | ||||||
|  |      */ | ||||||
|  |     async showTour(): Promise<void> { | ||||||
|  |         const nativeButton = this.button?.nativeElement.shadowRoot?.children[0] as HTMLElement; | ||||||
|  | 
 | ||||||
|  |         if (!nativeButton) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await CoreUserTours.showIfPending({ | ||||||
|  |             id: 'side-blocks-button', | ||||||
|  |             component: CoreBlockSideBlocksTourComponent, | ||||||
|  |             focus: nativeButton, | ||||||
|  |             side: CoreUserToursSide.Start, | ||||||
|  |             alignment: CoreUserToursAlignment.Center, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,6 @@ | |||||||
|  | <h2>{{ 'core.block.tour_navigation_dashboard_title' | translate }}</h2> | ||||||
|  | <img src="assets/img/user-tours/side-blocks.svg" alt="" /> | ||||||
|  | <p>{{ 'core.block.tour_navigation_dashboard_content' | translate }}</p> | ||||||
|  | <ion-button (click)="dismiss()" expand="block"> | ||||||
|  |     {{ 'core.endonesteptour' | translate }} | ||||||
|  | </ion-button> | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | :host { | ||||||
|  | 
 | ||||||
|  |     h2 { | ||||||
|  |         margin-top: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     p { | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ion-button { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { CoreUserTours } from '@features/usertours/services/user-tours'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component showing the User Tour for the Side Blocks feature. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-block-side-blocks-tour', | ||||||
|  |     templateUrl: 'side-blocks-tour.html', | ||||||
|  |     styleUrls: ['side-blocks-tour.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreBlockSideBlocksTourComponent { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Dismiss User Tour. | ||||||
|  |      */ | ||||||
|  |     async dismiss(): Promise<void> { | ||||||
|  |         await CoreUserTours.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1,5 +1,7 @@ | |||||||
| { | { | ||||||
|     "blocks": "Blocks", |     "blocks": "Blocks", | ||||||
|     "noblocks": "No blocks found!", |     "noblocks": "No blocks found!", | ||||||
|     "opendrawerblocks": "Open block drawer" |     "opendrawerblocks": "Open block drawer", | ||||||
|  |     "tour_navigation_dashboard_content": "This side panel can contain more features.", | ||||||
|  |     "tour_navigation_dashboard_title": "Expand to explore" | ||||||
| } | } | ||||||
|  | |||||||
| @ -28,6 +28,7 @@ import { CoreCourseModuleInfoComponent } from './module-info/module-info'; | |||||||
| import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; | import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; | ||||||
| import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; | import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; | ||||||
| import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary'; | import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary'; | ||||||
|  | import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -39,6 +40,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar | |||||||
|         CoreCourseModuleInfoComponent, |         CoreCourseModuleInfoComponent, | ||||||
|         CoreCourseModuleManualCompletionComponent, |         CoreCourseModuleManualCompletionComponent, | ||||||
|         CoreCourseCourseIndexComponent, |         CoreCourseCourseIndexComponent, | ||||||
|  |         CoreCourseCourseIndexTourComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|         CoreCourseModuleNavigationComponent, |         CoreCourseModuleNavigationComponent, | ||||||
| @ -57,6 +59,7 @@ import { CoreCourseModuleSummaryComponent } from './module-summary/module-summar | |||||||
|         CoreCourseModuleInfoComponent, |         CoreCourseModuleInfoComponent, | ||||||
|         CoreCourseModuleManualCompletionComponent, |         CoreCourseModuleManualCompletionComponent, | ||||||
|         CoreCourseCourseIndexComponent, |         CoreCourseCourseIndexComponent, | ||||||
|  |         CoreCourseCourseIndexTourComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|         CoreCourseModuleNavigationComponent, |         CoreCourseModuleNavigationComponent, | ||||||
|  | |||||||
| @ -49,7 +49,8 @@ | |||||||
| 
 | 
 | ||||||
| <!-- Course Index button. --> | <!-- Course Index button. --> | ||||||
| <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex"> | <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex"> | ||||||
|     <ion-fab-button (click)="openCourseIndex()" [attr.aria-label]="'core.course.courseindex' | translate" color="secondary"> |     <ion-fab-button (click)="openCourseIndex()" (onAppear)="showCourseIndexTour()" [attr.aria-label]="'core.course.courseindex' | translate" | ||||||
|  |         color="secondary" #courseIndexFab> | ||||||
|         <ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon> |         <ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon> | ||||||
|         <span class="sr-only">{{'core.course.courseindex' | translate }}</span> |         <span class="sr-only">{{'core.course.courseindex' | translate }}</span> | ||||||
|     </ion-fab-button> |     </ion-fab-button> | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import { | |||||||
|     QueryList, |     QueryList, | ||||||
|     Type, |     Type, | ||||||
|     ElementRef, |     ElementRef, | ||||||
|  |     ViewChild, | ||||||
| } from '@angular/core'; | } from '@angular/core'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||||
| @ -44,6 +45,8 @@ import { CoreBlockHelper } from '@features/block/services/block-helper'; | |||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||||
| import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course'; | import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course'; | ||||||
|  | import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours'; | ||||||
|  | import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component to display course contents using a certain format. If the format isn't found, use default one. |  * Component to display course contents using a certain format. If the format isn't found, use default one. | ||||||
| @ -71,6 +74,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
 |     @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
 | ||||||
| 
 | 
 | ||||||
|     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; |     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; | ||||||
|  |     @ViewChild('courseIndexFab', { read: ElementRef }) courseIndexFab?: ElementRef<HTMLElement>; | ||||||
| 
 | 
 | ||||||
|     // All the possible component classes.
 |     // All the possible component classes.
 | ||||||
|     courseFormatComponent?: Type<unknown>; |     courseFormatComponent?: Type<unknown>; | ||||||
| @ -160,6 +164,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Show Course Index User Tour. | ||||||
|  |      */ | ||||||
|  |     async showCourseIndexTour(): Promise<void> { | ||||||
|  |         const nativeButton = this.courseIndexFab?.nativeElement.shadowRoot?.children[0] as HTMLElement; | ||||||
|  | 
 | ||||||
|  |         if (!nativeButton) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await CoreUserTours.showIfPending({ | ||||||
|  |             id: 'course-index', | ||||||
|  |             component: CoreCourseCourseIndexTourComponent, | ||||||
|  |             focus: nativeButton, | ||||||
|  |             side: CoreUserToursSide.Top, | ||||||
|  |             alignment: CoreUserToursAlignment.End, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Detect changes on input properties. |      * Detect changes on input properties. | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -0,0 +1,6 @@ | |||||||
|  | <h2>{{ 'core.course.tour_navigation_course_index_student_title' | translate }}</h2> | ||||||
|  | <img src="assets/img/user-tours/course-index.svg" alt="" /> | ||||||
|  | <p>{{ 'core.course.tour_navigation_course_index_student_content' | translate }}</p> | ||||||
|  | <ion-button (click)="dismiss()" expand="block"> | ||||||
|  |     {{ 'core.endonesteptour' | translate }} | ||||||
|  | </ion-button> | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | :host { | ||||||
|  | 
 | ||||||
|  |     h2 { | ||||||
|  |         margin-top: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     p { | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ion-button { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { CoreUserTours } from '@features/usertours/services/user-tours'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component showing the User Tour for the Course Index feature. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-course-course-index-tour', | ||||||
|  |     templateUrl: 'course-index-tour.html', | ||||||
|  |     styleUrls: ['course-index-tour.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreCourseCourseIndexTourComponent { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Dismiss the User Tour. | ||||||
|  |      */ | ||||||
|  |     async dismiss(): Promise<void> { | ||||||
|  |         await CoreUserTours.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -54,6 +54,8 @@ | |||||||
|     "startdate": "Course start date", |     "startdate": "Course start date", | ||||||
|     "thisweek": "This week", |     "thisweek": "This week", | ||||||
|     "todo": "To do", |     "todo": "To do", | ||||||
|  |     "tour_navigation_course_index_student_content": "Browse through activities and track your progress.", | ||||||
|  |     "tour_navigation_course_index_student_title": "Find your way around", | ||||||
|     "useactivityonbrowser": "You can still use it using your device's web browser.", |     "useactivityonbrowser": "You can still use it using your device's web browser.", | ||||||
|     "viewcourse": "View course", |     "viewcourse": "View course", | ||||||
|     "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", |     "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", | ||||||
|  | |||||||
| @ -37,6 +37,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module'; | |||||||
| import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; | import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; | ||||||
| import { CoreStylesModule } from './styles/styles.module'; | import { CoreStylesModule } from './styles/styles.module'; | ||||||
| import { CoreTagModule } from './tag/tag.module'; | import { CoreTagModule } from './tag/tag.module'; | ||||||
|  | import { CoreUserToursModule } from './usertours/user-tours.module'; | ||||||
| import { CoreUserModule } from './user/user.module'; | import { CoreUserModule } from './user/user.module'; | ||||||
| import { CoreViewerModule } from './viewer/viewer.module'; | import { CoreViewerModule } from './viewer/viewer.module'; | ||||||
| import { CoreXAPIModule } from './xapi/xapi.module'; | import { CoreXAPIModule } from './xapi/xapi.module'; | ||||||
| @ -66,6 +67,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | |||||||
|         CoreSitePluginsModule, |         CoreSitePluginsModule, | ||||||
|         CoreTagModule, |         CoreTagModule, | ||||||
|         CoreStylesModule, |         CoreStylesModule, | ||||||
|  |         CoreUserToursModule, | ||||||
|         CoreUserModule, |         CoreUserModule, | ||||||
|         CoreViewerModule, |         CoreViewerModule, | ||||||
|         CoreXAPIModule, |         CoreXAPIModule, | ||||||
|  | |||||||
| @ -17,11 +17,13 @@ import { CoreSharedModule } from '@/core/shared.module'; | |||||||
| import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; | import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; | ||||||
| import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; | import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; | ||||||
| import { CoreLoginComponentsModule } from '@features/login/components/components.module'; | import { CoreLoginComponentsModule } from '@features/login/components/components.module'; | ||||||
|  | import { CoreMainMenuUserMenuTourComponent } from './user-menu-tour/user-menu-tour'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
|         CoreMainMenuUserButtonComponent, |         CoreMainMenuUserButtonComponent, | ||||||
|         CoreMainMenuUserMenuComponent, |         CoreMainMenuUserMenuComponent, | ||||||
|  |         CoreMainMenuUserMenuTourComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CoreSharedModule, |         CoreSharedModule, | ||||||
| @ -30,6 +32,7 @@ import { CoreLoginComponentsModule } from '@features/login/components/components | |||||||
|     exports: [ |     exports: [ | ||||||
|         CoreMainMenuUserButtonComponent, |         CoreMainMenuUserButtonComponent, | ||||||
|         CoreMainMenuUserMenuComponent, |         CoreMainMenuUserMenuComponent, | ||||||
|  |         CoreMainMenuUserMenuTourComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreMainMenuComponentsModule {} | export class CoreMainMenuComponentsModule {} | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
| <core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false" | <core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false" | ||||||
|     (ariaButtonClick)="openUserMenu($event)" role="button" tabindex="0" [attr.aria-label]="'core.user.useraccount' | translate"> |     (ariaButtonClick)="openUserMenu($event)" (onAppear)="showTour()" role="button" tabindex="0" | ||||||
|  |     [attr.aria-label]="'core.user.useraccount' | translate" #avatar> | ||||||
| </core-user-avatar> | </core-user-avatar> | ||||||
|  | |||||||
| @ -12,11 +12,13 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; | ||||||
| import { CoreSiteInfo } from '@classes/site'; | import { CoreSiteInfo } from '@classes/site'; | ||||||
|  | import { CoreUserTours, CoreUserToursStyle } from '@features/usertours/services/user-tours'; | ||||||
| import { IonRouterOutlet } from '@ionic/angular'; | import { IonRouterOutlet } from '@ionic/angular'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreMainMenuUserMenuTourComponent } from '../user-menu-tour/user-menu-tour'; | ||||||
| import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu'; | import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -34,6 +36,8 @@ export class CoreMainMenuUserButtonComponent implements OnInit { | |||||||
|     siteInfo?: CoreSiteInfo; |     siteInfo?: CoreSiteInfo; | ||||||
|     isMainScreen = false; |     isMainScreen = false; | ||||||
| 
 | 
 | ||||||
|  |     @ViewChild('avatar', { read: ElementRef }) avatar?: ElementRef<HTMLElement>; | ||||||
|  | 
 | ||||||
|     constructor(protected routerOutlet: IonRouterOutlet) { |     constructor(protected routerOutlet: IonRouterOutlet) { | ||||||
|         const currentSite = CoreSites.getRequiredCurrentSite(); |         const currentSite = CoreSites.getRequiredCurrentSite(); | ||||||
| 
 | 
 | ||||||
| @ -61,4 +65,20 @@ export class CoreMainMenuUserButtonComponent implements OnInit { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Show User Tour. | ||||||
|  |      */ | ||||||
|  |     async showTour(): Promise<void> { | ||||||
|  |         if (!this.avatar) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await CoreUserTours.showIfPending({ | ||||||
|  |             id: 'user-menu', | ||||||
|  |             component: CoreMainMenuUserMenuTourComponent, | ||||||
|  |             focus: this.avatar.nativeElement, | ||||||
|  |             style: CoreUserToursStyle.Overlay, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,6 @@ | |||||||
|  | <img src="assets/img/user-tours/user-menu.svg" alt="" /> | ||||||
|  | <h2>{{ 'core.mainmenu.usermenutourtitle' | translate }}</h2> | ||||||
|  | <p>{{ 'core.mainmenu.usermenutourdescription' | translate }}</p> | ||||||
|  | <ion-button (click)="dismiss()" expand="block"> | ||||||
|  |     {{ 'core.endonesteptour' | translate }} | ||||||
|  | </ion-button> | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | :host { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     max-width: 85vw; | ||||||
|  |     align-items: center; | ||||||
|  |     flex-direction: column; | ||||||
|  | 
 | ||||||
|  |     img { | ||||||
|  |         width: calc(100vw - var(--core-avatar-size) * 2 - 16px); | ||||||
|  |         margin-top: 12px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     p { | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ion-button { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | :host-context([dir=rtl]) img { | ||||||
|  |     transform: scaleX(-1); | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { CoreUserTours } from '@features/usertours/services/user-tours'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component showing the User Tour for the User Menu feature. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-mainmenu-user-menu-tour', | ||||||
|  |     templateUrl: 'user-menu-tour.html', | ||||||
|  |     styleUrls: ['user-menu-tour.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreMainMenuUserMenuTourComponent { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Dismiss the User Tour. | ||||||
|  |      */ | ||||||
|  |     async dismiss(): Promise<void> { | ||||||
|  |         await CoreUserTours.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1,5 +1,7 @@ | |||||||
| { | { | ||||||
|     "home": "Home", |     "home": "Home", | ||||||
|     "logout": "Log out", |     "logout": "Log out", | ||||||
|     "switchaccount": "Switch account" |     "switchaccount": "Switch account", | ||||||
|  |     "usermenutourdescription": "The place to check your grades, change your preferences or switch accounts.", | ||||||
|  |     "usermenutourtitle": "Explore your personal area" | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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", |     "submit": "Submit", | ||||||
|     "success": "Success", |     "success": "Success", | ||||||
|     "summary": "Summary", |     "summary": "Summary", | ||||||
|  |     "swipenavigationtourdescription": "Swipe left and right to navigate around.", | ||||||
|     "tablet": "Tablet", |     "tablet": "Tablet", | ||||||
|     "teachers": "Teachers", |     "teachers": "Teachers", | ||||||
|     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", |     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", | ||||||
| @ -329,6 +330,7 @@ | |||||||
|     "usernotfullysetup": "User not fully set-up", |     "usernotfullysetup": "User not fully set-up", | ||||||
|     "usernologin": "Authentication has been revoked for this account", |     "usernologin": "Authentication has been revoked for this account", | ||||||
|     "usersuspended": "Registration suspended", |     "usersuspended": "Registration suspended", | ||||||
|  |     "endonesteptour": "Got it", | ||||||
|     "users": "Users", |     "users": "Users", | ||||||
|     "view": "View", |     "view": "View", | ||||||
|     "viewcode": "View code", |     "viewcode": "View code", | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { CoreEvents } from '@singletons/events'; | |||||||
| import { CoreDatabaseTable } from '@classes/database/database-table'; | import { CoreDatabaseTable } from '@classes/database/database-table'; | ||||||
| import { asyncInstance } from '../utils/async-instance'; | import { asyncInstance } from '../utils/async-instance'; | ||||||
| import { CorePromisedValue } from '@classes/promised-value'; | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
| import { CoreUtils } from './utils/utils'; | import { CoreBrowser } from '@singletons/browser'; | ||||||
| 
 | 
 | ||||||
| declare module '@singletons/events' { | declare module '@singletons/events' { | ||||||
| 
 | 
 | ||||||
| @ -195,11 +195,11 @@ export class CoreConfigProvider { | |||||||
|      * Load development config overrides. |      * Load development config overrides. | ||||||
|      */ |      */ | ||||||
|     protected loadDevelopmentConfig(): void { |     protected loadDevelopmentConfig(): void { | ||||||
|         if (!CoreConstants.enableDevTools() || !CoreUtils.hasCookie('MoodleAppConfig')) { |         if (!CoreConstants.enableDevTools() || !CoreBrowser.hasCookie('MoodleAppConfig')) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.patchEnvironment(JSON.parse(CoreUtils.getCookie('MoodleAppConfig') ?? '{}')); |         this.patchEnvironment(JSON.parse(CoreBrowser.getCookie('MoodleAppConfig') ?? '{}')); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -16,9 +16,9 @@ import { Injectable } from '@angular/core'; | |||||||
| 
 | 
 | ||||||
| import { SQLiteDB } from '@classes/sqlitedb'; | import { SQLiteDB } from '@classes/sqlitedb'; | ||||||
| import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; | import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; | ||||||
|  | import { CoreBrowser } from '@singletons/browser'; | ||||||
| import { makeSingleton, SQLite, Platform } from '@singletons'; | import { makeSingleton, SQLite, Platform } from '@singletons'; | ||||||
| import { CoreAppProvider } from './app'; | import { CoreAppProvider } from './app'; | ||||||
| import { CoreUtils } from './utils/utils'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This service allows interacting with the local database to store and retrieve data. |  * This service allows interacting with the local database to store and retrieve data. | ||||||
| @ -36,7 +36,7 @@ export class CoreDbProvider { | |||||||
|      * @returns Whether queries should be logged. |      * @returns Whether queries should be logged. | ||||||
|      */ |      */ | ||||||
|     loggingEnabled(): boolean { |     loggingEnabled(): boolean { | ||||||
|         return CoreUtils.hasCookie('MoodleAppDBLoggingEnabled') || CoreAppProvider.isAutomated(); |         return CoreBrowser.hasCookie('MoodleAppDBLoggingEnabled') || CoreAppProvider.isAutomated(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -806,6 +806,24 @@ export class CoreDomUtilsProvider { | |||||||
|         return elementPoint > window.innerHeight || elementPoint < scrollTopPos; |         return elementPoint > window.innerHeight || elementPoint < scrollTopPos; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether an element is visible or not. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      */ | ||||||
|  |     isElementVisible(element: HTMLElement): boolean { | ||||||
|  |         if (element.clientWidth === 0 || element.clientHeight === 0) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const style = getComputedStyle(element); | ||||||
|  |         if (style.opacity === '0' || style.display === 'none' || style.visibility === 'hidden') { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return element.offsetParent !== null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if rich text editor is enabled. |      * Check if rich text editor is enabled. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -1765,34 +1765,6 @@ export class CoreUtilsProvider { | |||||||
|         return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; |         return CoreApp.isIOS() && openFileAction == OpenFileAction.OPEN_WITH; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Check whether the given cookie is set. |  | ||||||
|      * |  | ||||||
|      * @param name Cookie name. |  | ||||||
|      * @returns Whether the cookie is set. |  | ||||||
|      */ |  | ||||||
|     hasCookie(name: string): boolean { |  | ||||||
|         return new RegExp(`(\\s|;|^)${name}=`).test(document.cookie ?? ''); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Read a cookie. |  | ||||||
|      * |  | ||||||
|      * @param name Cookie name. |  | ||||||
|      * @return Cookie value. |  | ||||||
|      */ |  | ||||||
|     getCookie(name: string): string | null { |  | ||||||
|         const cookies = (document.cookie ?? '').split(';').reduce((cookies, cookie) => { |  | ||||||
|             const [name, value] = cookie.trim().split('='); |  | ||||||
| 
 |  | ||||||
|             cookies[name] = value; |  | ||||||
| 
 |  | ||||||
|             return cookies; |  | ||||||
|         }, {}); |  | ||||||
| 
 |  | ||||||
|         return cookies[name] ?? null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const CoreUtils = makeSingleton(CoreUtilsProvider); | export const CoreUtils = makeSingleton(CoreUtilsProvider); | ||||||
|  | |||||||
							
								
								
									
										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; |             : null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get a component instances and fail if it cannot be resolved. | ||||||
|  |      * | ||||||
|  |      * @param element Root element. | ||||||
|  |      * @param componentClass Component class. | ||||||
|  |      * @returns Component instance. | ||||||
|  |      */ | ||||||
|  |     static require<T>(element: Element, componentClass?: ComponentConstructor<T>): T { | ||||||
|  |         const instance = this.resolve(element, componentClass); | ||||||
|  | 
 | ||||||
|  |         if (!instance) { | ||||||
|  |             throw new Error('Couldn\'t resolve component instance'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Waits all elements to be rendered. |      * Waits all elements to be rendered. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -12,13 +12,22 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { AbstractType, ApplicationInitStatus, ApplicationRef, Injector, NgZone as NgZoneService, Type } from '@angular/core'; | import { | ||||||
|  |     AbstractType, | ||||||
|  |     ApplicationInitStatus, | ||||||
|  |     ApplicationRef, | ||||||
|  |     ComponentFactoryResolver as ComponentFactoryResolverService, | ||||||
|  |     Injector, | ||||||
|  |     NgZone as NgZoneService, | ||||||
|  |     Type, | ||||||
|  | } from '@angular/core'; | ||||||
| import { Router as RouterService } from '@angular/router'; | import { Router as RouterService } from '@angular/router'; | ||||||
| import { HttpClient } from '@angular/common/http'; | import { HttpClient } from '@angular/common/http'; | ||||||
| import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser'; | import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|     Platform as PlatformService, |     Platform as PlatformService, | ||||||
|  |     AngularDelegate as AngularDelegateService, | ||||||
|     AlertController as AlertControllerService, |     AlertController as AlertControllerService, | ||||||
|     LoadingController as LoadingControllerService, |     LoadingController as LoadingControllerService, | ||||||
|     ModalController as ModalControllerService, |     ModalController as ModalControllerService, | ||||||
| @ -58,11 +67,13 @@ import { Zip as ZipService } from '@ionic-native/zip/ngx'; | |||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| 
 | 
 | ||||||
| import { CoreApplicationInitStatus } from '@classes/application-init-status'; | import { CoreApplicationInitStatus } from '@classes/application-init-status'; | ||||||
|  | import { asyncInstance } from '@/core/utils/async-instance'; | ||||||
|  | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Injector instance used to resolve singletons. |  * Injector instance used to resolve singletons. | ||||||
|  */ |  */ | ||||||
| let singletonsInjector: Injector | null = null; | const singletonsInjector = new CorePromisedValue<Injector>(); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Helper to create a method that proxies calls to the underlying singleton instance. |  * Helper to create a method that proxies calls to the underlying singleton instance. | ||||||
| @ -87,7 +98,7 @@ export type CoreSingletonProxy<Service = unknown> = Service & { | |||||||
|  * @param injector Module injector. |  * @param injector Module injector. | ||||||
|  */ |  */ | ||||||
| export function setSingletonsInjector(injector: Injector): void { | export function setSingletonsInjector(injector: Injector): void { | ||||||
|     singletonsInjector = injector; |     singletonsInjector.resolve(injector); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -127,11 +138,13 @@ export function makeSingleton<Service extends object = object>( // eslint-disabl | |||||||
| 
 | 
 | ||||||
|     Object.defineProperty(singleton, 'instance', { |     Object.defineProperty(singleton, 'instance', { | ||||||
|         get: () => { |         get: () => { | ||||||
|             if (!singletonsInjector) { |             const injector = singletonsInjector.value; | ||||||
|  | 
 | ||||||
|  |             if (!injector) { | ||||||
|                 throw new Error('Can\'t resolve a singleton instance without an injector'); |                 throw new Error('Can\'t resolve a singleton instance without an injector'); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const instance = singletonsInjector.get(injectionToken); |             const instance = injector.get(injectionToken); | ||||||
| 
 | 
 | ||||||
|             singleton.setInstance(instance); |             singleton.setInstance(instance); | ||||||
| 
 | 
 | ||||||
| @ -194,7 +207,9 @@ export const NgZone = makeSingleton(NgZoneService); | |||||||
| export const Http = makeSingleton(HttpClient); | export const Http = makeSingleton(HttpClient); | ||||||
| export const Platform = makeSingleton(PlatformService); | export const Platform = makeSingleton(PlatformService); | ||||||
| export const ActionSheetController = makeSingleton(ActionSheetControllerService); | export const ActionSheetController = makeSingleton(ActionSheetControllerService); | ||||||
|  | export const AngularDelegate = makeSingleton(AngularDelegateService); | ||||||
| export const AlertController = makeSingleton(AlertControllerService); | export const AlertController = makeSingleton(AlertControllerService); | ||||||
|  | export const ComponentFactoryResolver = makeSingleton(ComponentFactoryResolverService); | ||||||
| export const LoadingController = makeSingleton(LoadingControllerService); | export const LoadingController = makeSingleton(LoadingControllerService); | ||||||
| export const ModalController = makeSingleton(ModalControllerService); | export const ModalController = makeSingleton(ModalControllerService); | ||||||
| export const PopoverController = makeSingleton(PopoverControllerService); | export const PopoverController = makeSingleton(PopoverControllerService); | ||||||
| @ -208,3 +223,10 @@ export const DomSanitizer = makeSingleton(DomSanitizerService); | |||||||
| 
 | 
 | ||||||
| // Convert external libraries injectables.
 | // Convert external libraries injectables.
 | ||||||
| export const Translate = makeSingleton(TranslateService); | export const Translate = makeSingleton(TranslateService); | ||||||
|  | 
 | ||||||
|  | // Async singletons.
 | ||||||
|  | export const AngularFrameworkDelegate = asyncInstance(async () => { | ||||||
|  |     const injector = await singletonsInjector; | ||||||
|  | 
 | ||||||
|  |     return AngularDelegate.create(ComponentFactoryResolver.instance, injector); | ||||||
|  | }); | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ import moment from 'moment'; | |||||||
| import { CoreConstants } from '@/core/constants'; | import { CoreConstants } from '@/core/constants'; | ||||||
| 
 | 
 | ||||||
| import { CoreTime } from './time'; | import { CoreTime } from './time'; | ||||||
|  | import { CoreBrowser } from '@singletons/browser'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Method to warn that logs are disabled, called only once. |  * Method to warn that logs are disabled, called only once. | ||||||
| @ -67,7 +68,10 @@ export class CoreLogger { | |||||||
|      */ |      */ | ||||||
|     static getInstance(className: string): CoreLogger { |     static getInstance(className: string): CoreLogger { | ||||||
|         // Disable log on production and testing.
 |         // Disable log on production and testing.
 | ||||||
|         if (CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting) { |         if ( | ||||||
|  |             !CoreBrowser.hasCookie('MoodleAppLoggingEnabled') && | ||||||
|  |             (CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting) | ||||||
|  |         ) { | ||||||
|             if (CoreConstants.BUILD.isProduction) { |             if (CoreConstants.BUILD.isProduction) { | ||||||
|                 warnLogsDisabled(); |                 warnLogsDisabled(); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -54,6 +54,20 @@ function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>): | |||||||
| 
 | 
 | ||||||
|             promisedInstance.resolve(instance); |             promisedInstance.resolve(instance); | ||||||
|         }, |         }, | ||||||
|  |         setLazyConstructor(constructor) { | ||||||
|  |             if (!promisedInstance) { | ||||||
|  |                 lazyConstructor = constructor; | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!promisedInstance.isResolved()) { | ||||||
|  |                 // eslint-disable-next-line promise/catch-or-return
 | ||||||
|  |                 Promise | ||||||
|  |                     .resolve(constructor()) | ||||||
|  |                     .then(instance => promisedInstance?.isResolved() || promisedInstance?.resolve(instance)); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         resetInstance() { |         resetInstance() { | ||||||
|             if (!promisedInstance) { |             if (!promisedInstance) { | ||||||
|                 return; |                 return; | ||||||
| @ -72,6 +86,7 @@ export interface AsyncInstanceWrapper<T> { | |||||||
|     getInstance(): Promise<T>; |     getInstance(): Promise<T>; | ||||||
|     getProperty<P extends keyof T>(property: P): Promise<T[P]>; |     getProperty<P extends keyof T>(property: P): Promise<T[P]>; | ||||||
|     setInstance(instance: T): void; |     setInstance(instance: T): void; | ||||||
|  |     setLazyConstructor(lazyConstructor: () => T | Promise<T>): void; | ||||||
|     resetInstance(): void; |     resetInstance(): void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										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>; |     languages: Record<string, string>; | ||||||
|     databaseOptimizations?: Partial<CoreDatabaseConfiguration>; |     databaseOptimizations?: Partial<CoreDatabaseConfiguration>; | ||||||
|     databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>; |     databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>; | ||||||
|  |     disableUserTours?: boolean; | ||||||
|  |     disabledUserTours?: string[]; | ||||||
|     wsservice: string; |     wsservice: string; | ||||||
|     demo_sites: Record<string, CoreSitesDemoSiteData>; |     demo_sites: Record<string, CoreSitesDemoSiteData>; | ||||||
|     zoomlevels: Record<CoreZoomLevel, number>; |     zoomlevels: Record<CoreZoomLevel, number>; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user