commit
						30b8c506e7
					
				| @ -17,7 +17,7 @@ import { Injectable, ViewContainerRef, ComponentFactoryResolver } from '@angular | ||||
| import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; | ||||
| import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| // @todo import { CoreH5PPlayerComponent } from '@core/h5p/components/h5p-player/h5p-player';
 | ||||
| import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support the Display H5P filter. | ||||
| @ -80,32 +80,31 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle | ||||
|      * @return If async, promise resolved when done. | ||||
|      */ | ||||
|     handleHtml( | ||||
|         container: HTMLElement, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         viewContainerRef: ViewContainerRef, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         component?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         componentId?: string | number, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|         container: HTMLElement, | ||||
|         filter: CoreFilterFilter, | ||||
|         options: CoreFilterFormatTextOptions, | ||||
|         viewContainerRef: ViewContainerRef, | ||||
|         component?: string, | ||||
|         componentId?: string | number, | ||||
|         siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
 | ||||
|     ): void | Promise<void> { | ||||
|         // @todo
 | ||||
| 
 | ||||
|         // const placeholders = <HTMLElement[]> Array.from(container.querySelectorAll('div.core-h5p-tmp-placeholder'));
 | ||||
|         const placeholders = <HTMLElement[]> Array.from(container.querySelectorAll('div.core-h5p-tmp-placeholder')); | ||||
| 
 | ||||
|         // placeholders.forEach((placeholder) => {
 | ||||
|         //     const url = placeholder.getAttribute('data-player-src');
 | ||||
|         placeholders.forEach((placeholder) => { | ||||
|             const url = placeholder.getAttribute('data-player-src') || ''; | ||||
| 
 | ||||
|         //     Create the component to display the player.
 | ||||
|         //     const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent);
 | ||||
|         //     const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(factory);
 | ||||
|             // Create the component to display the player.
 | ||||
|             const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent); | ||||
|             const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(factory); | ||||
| 
 | ||||
|         //     componentRef.instance.src = url;
 | ||||
|         //     componentRef.instance.component = component;
 | ||||
|         //     componentRef.instance.componentId = componentId;
 | ||||
|             componentRef.instance.src = url; | ||||
|             componentRef.instance.component = component; | ||||
|             componentRef.instance.componentId = componentId; | ||||
| 
 | ||||
|         //     // Move the component to its right position.
 | ||||
|         //     placeholder.parentElement?.replaceChild(componentRef.instance.elementRef.nativeElement, placeholder);
 | ||||
|         // });
 | ||||
|             // Move the component to its right position.
 | ||||
|             placeholder.parentElement?.replaceChild(componentRef.instance.elementRef.nativeElement, placeholder); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
							
								
								
									
										14
									
								
								src/assets/img/icons/h5p.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/assets/img/icons/h5p.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 viewBox="0 0 345 150" style="enable-background:new 0 0 345 150;" xml:space="preserve"> | ||||
| <g> | ||||
| 	<path d="M325.7,14.7C317.6,6.9,305.3,3,289,3h-43.5H234v31h-66l-5.4,22.2c4.5-2.1,10.9-4.2,15.3-5.3c4.4-1.1,8.8-0.9,13.1-0.9 | ||||
| 		c14.6,0,26.5,4.5,35.6,13.3c9.1,8.8,13.6,20,13.6,33.4c0,9.4-2.3,18.5-7,27.2s-11.3,15.4-19.9,20c-3.1,1.6-6.5,3.1-10.2,4.1h42.4 | ||||
| 		H259V95h25c18.2,0,31.7-4.2,40.6-12.5s13.3-19.9,13.3-34.6C337.9,33.6,333.8,22.5,325.7,14.7z M288.7,60.6c-3.5,3-9.6,4.4-18.3,4.4 | ||||
| 		H259V33h13.2c8.4,0,14.2,1.5,17.2,4.7c3.1,3.2,4.6,6.9,4.6,11.5C294,53.9,292.2,57.6,288.7,60.6z"/> | ||||
| 	<path d="M176.5,76.3c-7.9,0-14.7,4.6-18,11.2L119,81.9L136.8,3h-23.6H101v62H51V3H7v145h44V95h50v53h12.2h42 | ||||
| 		c-6.7-2-12.5-4.6-17.2-8.1c-4.8-3.6-8.7-7.7-11.7-12.3c-3-4.6-5.3-9.7-7.3-16.5l39.6-5.7c3.3,6.6,10.1,11.1,17.9,11.1 | ||||
| 		c11.1,0,20.1-9,20.1-20.1S187.5,76.3,176.5,76.3z"/> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.1 KiB | 
| @ -1087,7 +1087,7 @@ export class SQLiteDB { | ||||
| } | ||||
| 
 | ||||
| export type SQLiteDBRecordValues = { | ||||
|     [key in string ]: SQLiteDBRecordValue | undefined; | ||||
|     [key in string ]: SQLiteDBRecordValue | undefined | null; | ||||
| }; | ||||
| 
 | ||||
| export type SQLiteDBQueryParams = { | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module'; | ||||
| import { CoreCoursesModule } from './courses/courses.module'; | ||||
| import { CoreEmulatorModule } from './emulator/emulator.module'; | ||||
| import { CoreFileUploaderModule } from './fileuploader/fileuploader.module'; | ||||
| import { CoreH5PModule } from './h5p/h5p.module'; | ||||
| import { CoreLoginModule } from './login/login.module'; | ||||
| import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; | ||||
| import { CoreSettingsModule } from './settings/settings.module'; | ||||
| @ -25,6 +26,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module'; | ||||
| import { CoreTagModule } from './tag/tag.module'; | ||||
| import { CoreUserModule } from './user/user.module'; | ||||
| import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module'; | ||||
| import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
| @ -39,6 +41,8 @@ import { CorePushNotificationsModule } from './pushnotifications/pushnotificatio | ||||
|         CoreTagModule, | ||||
|         CoreUserModule, | ||||
|         CorePushNotificationsModule, | ||||
|         CoreXAPIModule, | ||||
|         CoreH5PModule, | ||||
|     ], | ||||
| }) | ||||
| export class CoreFeaturesModule {} | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.eot
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										62
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| <?xml version="1.0" standalone="no"?> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > | ||||
| <svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"> | ||||
| <metadata> | ||||
| <json> | ||||
| <![CDATA[ | ||||
| { | ||||
| 	"fontFamily": "h5p-core-21", | ||||
| 	"description": "Font generated by IcoMoon.", | ||||
| 	"majorVersion": 1, | ||||
| 	"minorVersion": 1, | ||||
| 	"version": "Version 1.1", | ||||
| 	"fontId": "h5p-core-21", | ||||
| 	"psName": "h5p-core-21", | ||||
| 	"subFamily": "Regular", | ||||
| 	"fullName": "h5p-core-21" | ||||
| } | ||||
| ]]> | ||||
| </json> | ||||
| </metadata> | ||||
| <defs> | ||||
| <font id="h5p-core-21" horiz-adv-x="1024"> | ||||
| <font-face units-per-em="1024" ascent="960" descent="-64" /> | ||||
| <missing-glyph horiz-adv-x="1024" /> | ||||
| <glyph unicode=" " horiz-adv-x="512" d="" /> | ||||
| <glyph unicode="" glyph-name="arrow-down" data-tags="arrow-down" d="M234 389.669h556l-278-278z" /> | ||||
| <glyph unicode="" glyph-name="arrow-left" data-tags="arrow-left" d="M381-11.331v524l262-262z" /> | ||||
| <glyph unicode="" glyph-name="colapse" data-tags="colapse" d="M512 447.336l256-256-60-60-196 196-196-196-60 60z" /> | ||||
| <glyph unicode="" glyph-name="expand" data-tags="expand" d="M708 423.336l60-60-256-256-256 256 60 60 196-196z" /> | ||||
| <glyph unicode="" glyph-name="move" data-tags="move" d="M386.662 725.063h71.27v-71.27h-71.27v71.27zM566.067 725.063h71.27v-71.27h-71.27v71.27zM386.662 568.8h71.27v-71.27h-71.27v71.27zM566.067 568.8h71.27v-71.27h-71.27v71.27zM386.662 412.435h71.27v-71.27h-71.27v71.27zM566.067 412.435h71.27v-71.27h-71.27v71.27zM386.662 256.173h71.27v-71.27h-71.27v71.27zM566.067 256.173h71.27v-71.27h-71.27v71.27zM386.662 99.808h71.27v-71.27h-71.27v71.27zM566.067 99.808h71.27v-71.27h-71.27v71.27zM386.662-56.454h71.27v-71.27h-71.27v71.27zM566.067-56.454h71.27v-71.27h-71.27v71.27z" /> | ||||
| <glyph unicode="" glyph-name="check-mark" data-tags="check-mark" d="M454.299 245.924l-116.917 116.917-84.781-84.707 201.696-201.697 317.097 317.097-84.781 84.706z" /> | ||||
| <glyph unicode="" glyph-name="arrow-up-circle" data-tags="arrow-up-circle" d="M512 606.057c-148.616 0-264.722-120.75-260.077-269.367 0-125.395 88.241-232.212 208.991-255.434v213.636h-92.885c-13.933 0-13.933 9.288-9.288 18.577l139.327 171.838c4.645 9.288 13.933 9.288 23.221 4.645 0 0 4.645-4.645 4.645-4.645l139.327-171.838c9.288-9.288 4.645-18.577-9.288-18.577h-92.885v-213.636c143.972 32.51 232.212 171.838 199.703 315.808-23.221 120.75-130.039 204.347-250.789 208.991z" /> | ||||
| <glyph unicode="" glyph-name="info-circle" data-tags="info-circle" d="M512 601.601c-144.077 0-260.266-116.191-260.266-260.266s116.191-260.266 260.266-260.266 260.266 116.191 260.266 260.266v0c0 139.429-116.191 255.619-260.266 260.266zM470.171 550.478h88.305v-69.714h-88.305v69.714zM600.305 160.078h-181.257v51.123h51.123v162.666h-51.123v51.123h139.429v-218.438h46.477l-4.648-46.477z" /> | ||||
| <glyph unicode="" glyph-name="search" data-tags="search" d="M772.098 125.51l-110.68 110.68c71.943 99.612 49.806 243.494-49.806 315.437s-243.494 44.27-315.437-55.339c-71.943-99.612-49.806-243.494 49.806-315.437 77.475-55.339 182.623-55.339 260.098 0l110.68-110.68c5.533-5.533 11.068-5.533 16.601 0 0 0 0 0 0 0l33.205 33.205c11.068 5.533 11.068 16.601 5.533 22.137 0 0 0 0 0 0zM478.795 202.985c-88.544 0-160.486 71.943-160.486 160.486s71.943 160.486 160.486 160.486 160.486-71.943 160.486-160.486-71.943-160.486-160.486-160.486v0z" /> | ||||
| <glyph unicode="" glyph-name="fullscreen" data-tags="fullscreen" d="M368.55 490.521c5.737 5.737 0 5.737-5.737 5.737l-103.284 11.476c-5.737 5.737-11.476 0-11.476-5.737l11.476-109.021c0-5.737 5.737-5.737 5.737-5.737l103.284 103.284zM293.959 427.403l63.118-63.118c5.737-5.737 11.476-5.737 17.213 0l22.953 22.953c5.737 5.737 5.737 11.476 0 17.213l-63.118 57.379-40.166-34.429zM787.42 387.237c5.737-5.737 5.737 0 5.737 5.737l11.476 109.021c0 5.737-5.737 11.476-11.476 11.476l-109.021-11.476c-5.737 0-5.737-5.737-5.737-5.737l109.021-109.021zM724.305 461.832l-63.118-63.118c-5.737-5.737-5.737-11.476 0-17.213l22.953-22.953c5.737-5.737 11.476-5.737 17.213 0l63.118 63.118-40.166 40.166zM689.876 180.672c-5.737-5.737 0-5.737 5.737-5.737l109.021-11.476c5.737 0 11.476 5.737 11.476 11.476l-17.213 103.284c0 5.737-5.737 5.737-5.737 5.737l-103.284-103.284zM758.731 249.527l-63.118 63.118c-5.737 5.737-11.476 5.737-17.213 0l-22.953-22.953c-5.737-5.737-5.737-11.476 0-17.213l63.118-63.118 40.166 40.166zM265.269 283.956c-5.737 5.737-5.737 0-5.737-5.737l-11.476-109.021c0-5.737 5.737-11.476 11.476-11.476l109.021 11.476c5.737 0 5.737 5.737 5.737 5.737l-109.021 109.021zM334.124 209.362l63.118 63.118c5.737 5.737 5.737 11.476 0 17.213l-22.953 22.953c-5.737 5.737-11.476 5.737-17.213 0l-63.118-63.118 40.166-40.166zM161.985 593.805v-499.201h722.979v499.201h-722.979zM844.799 134.77h-636.911v413.13h636.911v-413.13z" /> | ||||
| <glyph unicode="" glyph-name="h5p" data-tags="h5p" d="M934.072 489.192c-22.319 16.738-50.216 27.897-89.273 27.897h-139.487v-66.954h-156.225l-11.159-55.795c11.159 5.579 27.897 11.159 39.057 11.159s22.319 0 33.476 0c33.476 0 66.954-11.159 89.273-33.476s33.476-50.216 33.476-83.692c0-22.319-5.579-44.635-16.738-66.954s-27.897-39.057-50.216-50.216c-5.579-5.579-16.738 0-22.319-11.159h117.17v133.908h66.954c44.635 0 78.113 11.159 100.43 27.897 22.319 22.319 33.476 50.216 33.476 83.692 0 39.057-11.159 66.954-27.897 83.692v0zM839.221 377.603c-11.159-5.579-22.319-11.159-44.635-11.159h-33.476v83.692h39.057c22.319 0 33.476-5.579 44.635-11.159 5.579-5.579 11.159-16.738 11.159-27.897 0-16.738-5.579-27.897-16.738-33.476v0zM565.826 338.546c-16.738 0-33.476-11.159-44.635-27.897l-94.851 16.738 44.635 195.281h-94.851v-150.646h-117.17v150.646h-111.589v-362.667h111.589v133.908h117.17v-133.908h139.487c-16.738 11.159-33.476 11.159-44.635 22.319s-22.319 22.319-27.897 33.476c-5.579 11.159-11.159 22.319-16.738 39.057l94.851 16.738c5.579-16.738 22.319-27.897 44.635-27.897 27.897 0 50.216 22.319 50.216 50.216 0 22.319-22.319 44.635-50.216 44.635v0z" /> | ||||
| <glyph unicode="" glyph-name="rights-of-use" data-tags="rights-of-use" d="M899.611 329.519c0-5.907 0-5.907 0-5.907-23.631-23.631-47.261-35.448-76.799-41.355-11.813 0-23.631-5.907-35.448-5.907s-17.724 0-29.537 0c0 0-5.907 0-5.907 5.907-64.985 59.079-135.877 118.153-200.863 183.139 0 0-5.907 0-5.907 0-23.631-5.907-47.261-11.813-70.892-17.724s-53.168 0-76.799 11.813c-17.724 11.813-23.631 17.724-29.537 35.448-5.907 5.907 0 23.631 11.813 23.631 41.355 11.813 88.616 29.537 129.971 47.261 11.813 5.907 29.537 5.907 41.355 5.907 5.907 0 11.813-5.907 11.813-5.907 41.355-17.724 82.709-29.537 124.060-47.261 0 0 5.907 0 5.907 0 29.537 5.907 64.985 17.724 94.523 23.631 5.907 0 5.907 0 5.907 0l106.34-212.676zM291.12 335.429c17.724 11.813 35.448 5.907 53.168-11.813 11.813-11.813 11.813-29.537 5.907-47.261 17.724 5.907 35.448-5.907 41.355-17.724 11.813-17.724 5.907-35.448-5.907-47.261 5.907 0 11.813 0 17.724 0 11.813-5.907 23.631-11.813 29.537-29.537s0-29.537-5.907-35.448c-5.907-5.907-11.813-11.813-17.724-17.724s-11.813-11.813-17.724-17.724-35.448-17.724-53.168 0c-29.537 29.537-53.168 64.985-82.709 94.523-17.724 23.631-35.448 41.355-47.261 64.985-5.907 11.813-11.813 17.724-11.813 29.537 0 5.907 0 17.724 5.907 23.631 11.813 11.813 17.724 17.724 29.537 29.537 17.724 17.724 47.261 11.813 64.985-5.907-5.907 0-5.907-5.907-5.907-11.813v0zM438.811 128.66l29.537-29.537c17.724-17.724 47.261-11.813 59.079 5.907l-5.907 5.907c-23.631 23.631-47.261 47.261-70.892 70.892-5.907 5.907-5.907 5.907-5.907 11.813s5.907 5.907 11.813 11.813c5.907 0 11.813 0 11.813-5.907 11.813-11.813 29.537-29.537 47.261-47.261 11.813-11.813 29.537-29.537 47.261-47.261 5.907-11.813 17.724-11.813 29.537-11.813 11.813 5.907 23.631 11.813 29.537 23.631 0 5.907 0 5.907 0 5.907-41.355 41.355-88.616 82.709-129.971 129.971-5.907 5.907-5.907 5.907-5.907 11.813 0 11.813 11.813 11.813 23.631 5.907 0 0 5.907 0 5.907-5.907 41.355-41.355 88.616-88.616 129.971-129.971 5.907-5.907 5.907-5.907 5.907-5.907 17.724 0 35.448 17.724 35.448 35.448 0 5.907 0 5.907 0 5.907-47.261 47.261-100.429 100.429-147.691 147.691-5.907 5.907-5.907 5.907-5.907 11.813s5.907 11.813 5.907 11.813c5.907 0 11.813 0 11.813-5.907 5.907-5.907 5.907-5.907 11.813-11.813 35.448-35.448 70.892-70.892 106.34-106.34 11.813-11.813 23.631-23.631 29.537-29.537 0 0 5.907-5.907 5.907 0 23.631 5.907 35.448 29.537 29.537 53.168h35.448c0 0 0 0 0 0 0-5.907 0-17.724 0-23.631-5.907-29.537-23.631-47.261-53.168-59.079 0 0-5.907 0-5.907-5.907-11.813-29.537-35.448-53.168-64.985-53.168-5.907 0-5.907 0-5.907-5.907-17.724-35.448-59.079-47.261-88.616-35.448-5.907 0-11.813 5.907-11.813 5.907-5.907-5.907-11.813-11.813-23.631-17.724-29.537-11.813-59.079-5.907-76.799 11.813-11.813 11.813-17.724 17.724-29.537 29.537 17.724 23.631 23.631 29.537 29.537 41.355v0 0zM273.396 642.628c29.537-11.813 64.985-23.631 94.523-29.537 35.448-11.813 64.985-23.631 100.429-29.537 0 0 0 0 5.907 0-17.724-5.907-35.448-11.813-47.261-17.724 0 0-5.907 0-5.907 0-47.261 11.813-94.523 23.631-135.877 41.355-5.907 0-5.907 0-5.907 0l-76.799-183.139c0-11.813 5.907-17.724 11.813-23.631s5.907-5.907 5.907-11.813c-5.907-5.907-11.813-17.724-23.631-23.631-17.724 17.724-29.537 35.448-29.537 64.985l88.616 212.676c-5.907-11.813 5.907 5.907 17.724 0v0z" /> | ||||
| <glyph unicode="" glyph-name="delete-circle" data-tags="delete-circle" d="M512 601.601c-147.107 0-260.266-118.817-260.266-260.266s118.817-260.266 260.266-260.266 260.266 118.817 260.266 260.266-113.158 260.266-260.266 260.266zM653.449 262.123c5.659-5.659 5.659-16.973 0-28.29l-33.949-33.949c-5.659-5.659-16.973-5.659-28.29 0l-79.212 79.212-79.212-79.212c-5.659-5.659-16.973-5.659-28.29 0l-33.949 33.949c-5.659 5.659-5.659 16.973 0 28.29l84.871 79.212-79.212 79.212c-5.659 5.659-5.659 16.973 0 28.29l33.949 33.949c5.659 5.659 16.973 5.659 28.29 0l73.554-84.871 79.212 79.212c5.659 5.659 16.973 5.659 28.29 0l33.949-33.949c5.659-5.659 5.659-16.973 0-28.29l-79.212-73.554 79.212-79.212z" /> | ||||
| <glyph unicode="" glyph-name="window" data-tags="window" d="M203.936 461.136c-5.704-5.704 0-5.704 5.704-5.704l108.394-11.41c5.704 0 11.41 5.704 11.41 11.41l-17.114 102.687c0 5.704-5.704 5.704-5.704 5.704l-102.687-102.687zM272.395 523.891l-62.752 62.752c-5.704 5.704-11.41 5.704-17.114 0l-17.114-22.821c-5.704-5.704-5.704-11.41 0-17.114l62.752-62.752 34.228 39.935zM751.605 558.119c-5.704 5.704-5.704 0-5.704-5.704l-11.41-108.394c0-5.704 5.704-11.41 11.41-11.41l108.394 11.41c5.704 0 5.704 5.704 5.704 5.704l-108.394 108.394zM814.357 483.957l62.752 62.752c5.704 5.704 5.704 11.41 0 17.114l-22.821 22.821c-5.704 5.704-11.41 5.704-17.114 0l-62.752-62.752 39.935-39.935zM848.588 221.534c5.704 5.704 0 5.704-5.704 5.704l-102.687 17.114c-5.704 0-11.41-5.704-11.41-11.41l11.41-108.394c0-5.704 5.704-5.704 5.704-5.704l102.687 102.687zM780.129 158.779l62.752-62.752c5.704-5.704 11.41-5.704 17.114 0l22.821 22.821c5.704 5.704 5.704 11.41 0 17.114l-62.752 62.752-39.935-39.935zM300.919 124.551c5.704-5.704 5.704 0 5.704 5.704l11.41 108.394c0 5.704-5.704 11.41-11.41 11.41l-108.394-11.41c-5.704 0-5.704-5.704-5.704-5.704l108.394-108.394zM238.167 193.010l-62.752-62.752c-5.704-5.704-5.704-11.41 0-17.114l22.821-22.821c5.704-5.704 11.41-5.704 17.114 0l62.752 62.752-39.935 39.935zM352.264 466.843v-239.605h347.998v239.605h-347.998zM654.622 267.172h-262.424v154.032h262.424v-154.032z" /> | ||||
| <glyph unicode="" glyph-name="code" data-tags="code" d="M449.641 235.325c6.235-6.235 6.235-12.472 6.235-18.707v-62.359c0-6.235-6.235-6.235-6.235-6.235l-230.728 155.897c-6.235 6.235-6.235 12.472-6.235 18.707v49.886c0 6.235 6.235 12.472 6.235 18.707l230.728 155.897c6.235 6.235 6.235 0 6.235-6.235v-62.359c0-6.235-6.235-12.472-6.235-18.707l-162.134-112.245c-6.235-6.235-6.235-6.235 0-12.472l162.134-99.776zM736.493 341.335c6.235 6.235 6.235 6.235 0 12.472l-155.897 112.245c-6.235 6.235-6.235 12.472-6.235 18.707v62.359c0 6.235 6.235 6.235 6.235 6.235l230.728-155.897c6.235-6.235 6.235-12.472 6.235-18.707v-49.886c0-6.235-6.235-12.472-6.235-18.707l-230.728-155.897c-6.235-6.235-6.235 0-6.235 6.235v62.359c0 6.235 6.235 12.472 6.235 18.707l155.897 99.776z" /> | ||||
| <glyph unicode="" glyph-name="download" data-tags="download" d="M358.941 435.525c-11.773 0-17.66-5.887-5.887-17.66l153.059-188.382c5.887-11.773 23.547-11.773 29.433 0l153.059 188.382c5.887 11.773 5.887 17.66-5.887 17.66h-323.782zM576.756 423.751v135.399c0 11.773-11.773 23.547-23.547 23.547h-70.643c-11.773 0-23.547-11.773-23.547-23.547v-141.286h117.739zM653.286 288.352c-5.887 0-17.66-5.887-23.547-11.773l-76.53-94.19c-5.887-5.887-17.66-17.66-23.547-23.547 0 0-5.887-5.887-11.773-5.887s-17.66 11.773-17.66 11.773c-5.887 5.887-17.66 17.66-23.547 23.547l-76.53 94.19c-5.887 5.887-17.66 11.773-23.547 11.773h-123.626c-5.887 0-17.66-5.887-17.66-17.66v-141.286c0-5.887 5.887-17.66 17.66-17.66h529.824c5.887 0 17.66 5.887 17.66 17.66v141.286c0 5.887-5.887 17.66-17.66 17.66l-129.513-5.887zM305.958 176.502c-17.66 0-29.433 11.773-29.433 29.433s11.773 29.433 29.433 29.433c17.66 0 29.433-11.773 29.433-29.433s-11.773-29.433-29.433-29.433v0z" /> | ||||
| <glyph unicode="" glyph-name="delete" data-tags="delete" d="M620.266 341.335l134.045 134.045c10.311 10.311 10.311 30.934 0 41.245l-61.866 61.866c-10.311 10.311-30.934 10.311-41.245 0l-134.045-134.045-134.045 134.045c-10.311 10.311-30.934 10.311-41.245 0l-61.866-61.866c-10.311-10.311-10.311-30.934 0-41.245l134.045-134.045-134.045-134.045c-10.311-10.311-10.311-30.934 0-41.245l61.866-61.866c10.311-10.311 30.934-10.311 41.245 0l134.045 134.045 134.045-134.045c10.311-10.311 30.934-10.311 41.245 0l61.866 61.866c10.311 10.311 10.311 30.934 0 41.245l-134.045 134.045z" /> | ||||
| <glyph unicode="" glyph-name="edit-image" data-tags="edit-image" d="M300.237 621.639c69.018 23.142 133.325 14.234 189.133-33.28 56.627-48.128 77.619-110.592 63.181-183.808-2.355-12.186 0.307-19.456 8.704-27.853 93.901-93.389 156.774-156.467 250.47-250.163 5.427-5.427 10.957-10.854 15.667-16.896 39.424-50.278 16.794-124.006-44.237-142.029-36.966-10.957-68.403 0-95.334 27.034-95.642 96.051-160.973 160.973-256.614 257.024-6.963 6.963-12.8 8.909-22.63 6.758-117.76-26.317-229.171 60.826-231.731 181.453-0.614 26.419 3.584 52.326 15.974 77.926 34.816-34.816 68.506-67.789 101.274-101.786 10.445-10.752 20.992-15.36 36.045-15.36 14.643 0 25.19 3.891 34.816 14.848 10.752 12.39 23.040 23.347 34.611 35.021 14.336 14.438 14.336 46.080-0.205 60.518-35.123 35.226-70.349 70.349-106.598 106.496 3.891 2.253 5.632 3.482 7.475 4.096zM703.386 63.559c-0.41-24.269 20.685-45.466 44.851-45.158 23.757 0.41 44.032 20.992 43.93 44.544-0.102 23.757-20.275 44.032-44.237 44.134-23.859 0.307-44.237-19.661-44.544-43.52z" /> | ||||
| <glyph unicode="" glyph-name="hourglass" data-tags="hourglass" d="M733.286-10.579c-147.763 0-295.526 0-443.29 0 0 2.048 0.102 4.096 0 6.144-0.307 13.824-1.024 32.666-0.922 46.49 0.41 39.731 6.861 78.131 19.046 115.2 17.203 52.224 43.725 96.256 81.306 130.355 4.506 4.096 9.216 7.885 13.722 11.776-0.205 0.717-0.307 1.126-0.41 1.229-1.331 1.229-2.765 2.355-4.198 3.584-28.058 22.63-50.688 51.405-68.403 85.606-30.618 59.085-43.52 123.597-41.165 192.614 0.205 7.168 0.614 18.33 0.922 25.498 147.763 0 295.526 0 443.29 0 0.205-1.331 0.512-2.662 0.614-3.994 2.662-36.966 1.229-77.722-5.939-113.869-14.336-72.909-44.544-133.837-95.027-179.405-4.096-3.686-8.294-7.066-12.39-10.547 0.205-0.717 0.307-1.126 0.512-1.331 0.819-0.717 1.638-1.434 2.458-2.15 42.189-33.894 71.68-79.872 90.931-135.782 11.776-34.202 18.637-69.837 20.070-106.701 0.819-19.763-0.614-44.851-1.126-64.717zM687.309 32.634c0 6.554 0.205 12.493 0 18.432-1.331 37.581-7.27 74.138-19.866 108.749-17.92 49.562-45.568 88.269-88.678 108.646-2.458 1.126-2.97 3.072-2.97 5.837 0.102 16.691 0.102 33.485 0 50.176 0 3.994 1.331 5.427 4.096 6.963 9.114 5.325 18.432 10.24 26.829 16.896 29.696 23.552 49.152 56.934 62.362 95.744 10.342 30.413 15.77 62.259 17.818 94.822 0.614 9.114 0.102 18.227 0.102 27.546-116.634 0-233.574 0-351.027 0 0.307-8.704 0.614-16.998 1.024-25.395 1.946-37.274 8.499-73.216 21.504-107.213 18.125-47.104 45.261-83.558 86.528-103.117 2.253-1.024 2.867-2.662 2.867-5.427-0.102-17.203-0.102-34.509 0-51.712 0-3.072-1.024-4.506-3.277-5.632-5.632-2.867-11.366-5.734-16.691-9.216-34.304-22.733-56.832-57.754-71.987-100.147-12.493-35.123-18.125-71.987-19.661-109.875-0.205-5.325 0-10.752 0-16.282 117.35 0.205 234.189 0.205 351.027 0.205zM410.214 451.962c68.096 0 135.373 0 203.674 0-3.789-6.554-7.168-12.595-10.752-18.227-10.957-16.998-24.269-30.618-39.731-41.472s-27.75-25.293-32.768-46.592c-1.638-6.963-2.765-14.336-2.867-21.606-0.307-17.203-0.205-34.406 0.307-51.712 0.717-28.058 12.493-48.947 32.154-62.566 43.008-30.003 65.843-75.878 75.776-132.506 0.307-1.638 0.307-3.379 0.614-5.53-83.149 0-166.093 0-249.242 0 2.662 20.685 7.885 40.346 15.462 59.085 13.312 32.973 32.768 59.187 59.494 77.722 16.589 11.469 28.365 27.955 32.358 50.995 0.819 4.813 1.331 9.83 1.434 14.746 0.102 18.637 0.614 37.274-0.205 55.808-1.126 24.678-11.981 43.213-28.57 57.139-9.216 7.782-18.944 14.746-27.648 23.142-11.981 11.162-21.197 25.395-29.491 41.574z" /> | ||||
| <glyph unicode="" glyph-name="plus-icon" data-tags="plus-icon" d="M768 285.015c0-19.323-15.664-34.987-34.987-34.987h-151.040v-151.467c0-19.323-15.664-34.987-34.987-34.987h-69.547c-19.323 0-34.987 15.664-34.987 34.987v151.467h-151.467c-19.323 0-34.987 15.664-34.987 34.987v69.547c0 19.323 15.664 34.987 34.987 34.987h151.467v151.467c0 19.323 15.664 34.987 34.987 34.987h69.547c19.323 0 34.987-15.664 34.987-34.987v-151.467h151.467c19.323 0 34.987-15.664 34.987-34.987z" /> | ||||
| <glyph unicode="" glyph-name="video-upload-icon" data-tags="video-upload-icon" d="M384 328.535v-128c0-21.333 21.333-42.667 42.667-42.667h128c21.333 0 42.667 17.067 42.667 42.667v128c0 21.333-17.067 42.667-42.667 42.667h-128c-21.333 0-42.667-17.067-42.667-42.667zM785.067 499.201l-102.4 106.667c-12.8 12.8-38.4 21.333-55.467 21.333h-140.8l21.333-42.667h89.6v-136.533c0-17.067 12.8-29.867 29.867-29.867h140.8v-341.333h-426.667v328.533h-42.667v-341.333c0-17.067 12.8-29.867 29.867-29.867h448c17.067 0 29.867 12.8 29.867 29.867v384c4.267 17.067-8.533 38.4-21.333 51.2zM640 456.535v123.733c4.267 0 12.8-4.267 12.8-8.533l102.4-102.4c4.267-4.267 4.267-8.533 8.533-12.8h-123.733zM725.333 166.401v196.267c0 4.267-4.267 8.533-8.533 8.533s-8.533 0-12.8-4.267l-89.6-89.6v-29.867l89.6-89.6c8.533-4.267 8.533 0 12.8 0 4.267 4.267 8.533 4.267 8.533 8.533zM349.867 473.601v136.533l59.733-59.733c8.533-8.533 17.067-4.267 25.6 0l12.8 12.8c8.533 8.533 8.533 17.067 0 25.6l-115.2 106.667c-8.533 8.533-17.067 8.533-21.333 0l-115.2-110.933c-4.267-8.533-4.267-17.067 0-25.6l12.8-12.8c8.533-8.533 17.067-8.533 21.333 0l59.733 59.733v-136.533c0-8.533 8.533-17.067 17.067-17.067h25.6c0 0 17.067 8.533 17.067 21.333z" /> | ||||
| <glyph unicode="" glyph-name="play-icon" data-tags="play-icon" d="M392.533 597.335c81.067 46.933 187.733 46.933 273.067 0 42.667-25.6 72.533-55.467 98.133-98.133 72.533-128 29.867-294.4-98.133-371.2-128-72.533-294.4-29.867-371.2 98.133-46.933 81.067-46.933 187.733 0 273.067 21.333 42.667 55.467 72.533 98.133 98.133zM661.333 345.601c12.8 8.533 12.8 29.867 0 38.4l-192 110.933c-8.533 4.267-12.8 4.267-21.333 0s-12.8-12.8-12.8-21.333v-226.133c0-8.533 4.267-17.067 12.8-21.333s17.067-4.267 21.333 0l192 119.467z" /> | ||||
| <glyph unicode="" glyph-name="copy" data-tags="copy" d="M722.133 561.201h-247.733c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h247.867c65.867 0 119.2 53.333 119.2 119.2v288.533c-0.267 65.867-53.467 119.2-119.333 119.2zM778.533 156.534c0-31.333-25.067-56.4-56.4-56.4h-247.867c-31.333 0-56.4 25.067-56.4 56.4v285.467c0 31.333 25.067 56.4 56.4 56.4h247.733c31.333 0 56.4-25.067 56.4-56.4v-285.467zM245.2 326.001v288.533c0 31.333 25.067 56.4 56.4 56.4h247.733c31.333 0 56.4-25.067 56.4-56.4v-18.8h62.667v18.8c0 65.867-53.333 119.2-119.2 119.2h-247.467c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h18.8v62.667h-18.8c-31.467-3.067-56.533 22-56.533 56.533zM681.2 360.534h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333zM681.2 266.401h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333zM681.2 172.268h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333z" /> | ||||
| <glyph unicode="" glyph-name="examples-icon" data-tags="examples-icon" d="M213.333 166.402c89.6 38.4 183.467 68.267 273.067 17.067v281.6c-68.267 46.933-157.867 55.467-234.667 12.8l-38.4-311.467zM810.667 166.402l-42.667 315.733c-72.533 38.4-166.4 34.133-234.667-17.067v-285.867c93.867 51.2 187.733 21.333 277.333-12.8zM832 520.535c-51.2 29.867-110.933 46.933-170.667 55.467-51.2 0-102.4-8.533-149.333-29.867-46.933 21.333-98.133 29.867-149.333 29.867-59.733-4.267-119.467-25.6-170.667-55.467l-64-452.267c0 0 29.867-17.067 110.933 21.333 46.933 25.6 102.4 34.133 157.867 25.6 42.667-4.267 85.333-21.333 115.2-55.467v0c29.867 34.133 72.533 51.2 115.2 55.467 55.467 4.267 106.667-4.267 157.867-29.867 81.067-38.4 110.933-21.333 110.933-21.333l-64 456.533zM793.6 115.202c-42.667 21.333-89.6 34.133-140.8 34.133-8.533 0-21.333 0-29.867 0-42.667-4.267-81.067-17.067-115.2-42.667-34.133 25.6-72.533 38.4-115.2 42.667-12.8 0-21.333 0-34.133 0-46.933 0-93.867-8.533-136.533-29.867-21.333-12.8-46.933-21.333-72.533-25.6l64 413.867c46.933 25.6 98.133 38.4 149.333 42.667 46.933 0 93.867-8.533 136.533-25.6l12.8-8.533 12.8 4.267c42.667 17.067 89.6 25.6 136.533 25.6 51.2-4.267 102.4-17.067 149.333-42.667l59.733-418.133c-29.867 8.533-51.2 17.067-76.8 29.867z" /> | ||||
| <glyph unicode="" glyph-name="tutorials-icon" data-tags="tutorials-icon" d="M887.467 430.935l-375.467-110.933h-4.267l-217.6 68.267c-21.333-25.6-34.133-59.733-34.133-98.133 21.333-12.8 25.6-38.4 12.8-59.733-4.267-4.267-8.533-8.533-12.8-12.8l17.067-145.067c0-4.267 0-4.267-4.267-8.533 0 0 0 0-4.267 0h-64c-4.267 0-4.267 0-8.533 4.267 0 4.267-4.267 4.267-4.267 8.533l17.067 145.067c-12.8 8.533-17.067 21.333-17.067 34.133 0 17.067 8.533 29.867 21.333 38.4 0 38.4 12.8 76.8 34.133 110.933l-106.667 29.867c-8.533 4.267-8.533 8.533-8.533 17.067 0 4.267 4.267 4.267 4.267 4.267l375.467 119.467h4.267l375.467-123.733c4.267 0 8.533-4.267 8.533-8.533s-4.267-8.533-8.533-12.8zM725.333 234.669c4.267-46.933-93.867-85.333-213.333-85.333s-213.333 38.4-213.333 85.333l4.267 106.667 192-64c4.267 0 12.8 0 17.067 0s12.8 0 17.067 4.267l192 59.733 4.267-106.667z" /> | ||||
| <glyph unicode="" glyph-name="info-important-description" data-tags="info-important-description" d="M512 697.368c-188.5 0-341.3-152.8-341.3-341.3s152.8-341.4 341.3-341.4 341.3 152.8 341.3 341.3-152.8 341.4-341.3 341.4v0zM512 43.268c-172.7 0-312.7 140-312.7 312.7s140 312.7 312.7 312.7c172.7 0 312.7-140 312.7-312.7-0.2-172.6-140.1-312.5-312.7-312.7v0zM512 605.568c-137.9 0-249.6-111.8-249.6-249.6s111.7-249.6 249.6-249.6 249.6 111.8 249.6 249.6-111.8 249.6-249.6 249.6v0z" /> | ||||
| <glyph unicode="" glyph-name="icon-info" data-tags="icon-info" d="M467.2 459.522h87.467c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v70.385c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-87.462c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-69.959c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.715-4.373 9.2-7.159 15.338-7.245zM597.333 156.589h-22.187v209.92c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-130.128c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h22.182v-139.947h-22.187c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h174.075c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v46.065c0.043 0.527 0.067 1.141 0.067 1.761 0 5.302-1.791 10.185-4.8 14.079-3.742 4.424-9.36 7.247-15.636 7.247-0.489 0-0.975-0.017-1.456-0.051z" /> | ||||
| <glyph unicode="" glyph-name="paste" data-tags="paste" d="M394.402 702.4h-75.333c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-56.4 37.6-100.4 87.867-116v69.067c-15.733 9.467-25.067 25.067-25.067 47.067v288.4c0 31.333 25.067 56.4 56.4 56.4h131.733c0 0 0 0 3.2 3.2v0c0 31.333-28.267 59.6-59.6 59.6zM704.802 592.533c0 0-28.267 0-40.8 0-12.533 34.533-43.867 59.6-84.667 59.6s-69.067-25.067-81.6-59.6c-12.533 0-40.8 0-40.8 0-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h247.867c65.867 0 119.2 53.333 119.2 119.2v285.467c3.2 65.867-53.2 122.267-119.2 122.267zM582.535 605.2c22 0 40.8-18.8 40.8-40.8s-18.8-40.8-40.8-40.8c-22 0-40.8 18.8-40.8 40.8s15.733 40.8 40.8 40.8zM764.402 181.733c0-31.333-25.067-56.4-56.4-56.4h-250.933c-31.333 0-56.4 25.067-56.4 56.4v288.533c0 18.8 9.467 37.6 25.067 47.067v0c0-43.867 34.533-78.4 78.4-78.4h160c43.867 0 78.4 34.533 78.4 78.4v0c12.533-9.467 22-28.267 22-47.067v-288.533z" /> | ||||
| <glyph unicode="" glyph-name="reuse" data-tags="reuse" d="M734.974 624.751c-54.605 61.619-134.123 100.573-222.977 100.573-164.936 0-298.661-133.724-298.661-298.661h74.667c0 123.721 100.272 223.993 223.993 223.993 68.214 0 128.747-30.96 169.766-79.119l-70.213-70.213h199.109v199.109l-75.689-75.689zM511.999 202.671c-68.214 0-128.747 30.96-169.766 79.119l70.213 70.213h-199.109v-199.109l75.689 75.689c54.605-61.619 134.123-100.573 222.977-100.573 164.936 0 298.661 133.724 298.661 298.661h-74.667c0-123.721-100.272-223.993-223.993-223.993z" /> | ||||
| <glyph unicode="" glyph-name="info-outlined" data-tags="info-outlined" d="M467.199 181.336h89.599v268.8h-89.599v-268.8zM512 853.335c-247.296 0-448.001-200.705-448.001-448.001s200.705-448.001 448.001-448.001 448.001 200.705 448.001 448.001-200.705 448.001-448.001 448.001zM512 46.936c-197.568 0-358.398 160.83-358.398 358.398s160.83 358.398 358.398 358.398 358.398-160.83 358.398-358.398-160.83-358.398-358.398-358.398zM467.199 539.734h89.599v89.599h-89.599v-89.599z" /> | ||||
| <glyph unicode="" glyph-name="spinner" data-tags="spinner" d="M600 808.001c0-48.602-39.398-88-88-88s-88 39.398-88 88 39.398 88 88 88 88-39.398 88-88zM512 133.333c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.398 88-88 88zM893.334 514.667c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.398 88-88 88zM218.666 426.667c0 48.602-39.398 88-88 88s-88-39.398-88-88 39.398-88 88-88 88 39.398 88 88zM242.357 245.024c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88c0 48.6-39.4 88-88 88zM781.643 245.024c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88c0 48.6-39.398 88-88 88zM242.357 784.31c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.4 88-88 88z" /> | ||||
| <glyph unicode="" glyph-name="copy-enabled" data-tags="copy-enabled" d="M614.525 809.292h-317.672c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-43.817 20.88-82.842 53.231-107.83v70.346c-4.45 11.297-7.016 23.962-7.016 37.484v370.388c0 49.292 40.224 89.516 89.516 89.516h318.014c15.232 0 29.611-3.766 42.107-10.613h68.294c-24.646 34.402-65.039 56.997-110.57 56.997zM674.431 267.403h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105zM674.431 388.244h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105zM726.978 686.23h-318.014c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-74.968 60.931-135.9 135.9-135.9v0h318.183c74.968 0 135.9 60.931 135.9 135.9v370.388c-0.342 74.968-61.273 135.9-136.073 135.9zM816.494 183.878c0-49.292-40.224-89.516-89.516-89.516h-318.183c-49.292 0-89.516 40.224-89.516 89.516v366.449c0 49.292 40.224 89.516 89.516 89.516h318.014c49.292 0 89.516-40.224 89.516-89.516v-349.334h0.173v-17.115zM674.431 509.081h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105z" /> | ||||
| <glyph unicode="" glyph-name="copy-disabled" data-tags="copy-disabled" d="M614.525 809.292h-317.672c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-43.817 20.88-82.842 53.231-107.83v70.346c-4.45 11.297-7.016 23.962-7.016 37.484v370.388c0 49.292 40.224 89.516 89.516 89.516h318.014c15.232 0 29.611-3.766 42.107-10.613h68.294c-24.646 34.402-65.039 56.997-110.57 56.997zM726.978 686.23h-318.014c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-74.968 60.931-135.9 135.9-135.9v0h318.183c74.968 0 135.9 60.931 135.9 135.9v370.388c-0.342 74.968-61.273 135.9-136.073 135.9zM816.494 183.878c0-49.292-40.224-89.516-89.516-89.516h-318.183c-49.292 0-89.516 40.224-89.516 89.516v366.449c0 49.292 40.224 89.516 89.516 89.516h318.014c49.292 0 89.516-40.224 89.516-89.516v-349.334h0.173v-17.115zM709.521 468.857l-27.728 27.555-109.544-109.544-109.544 109.544-27.555-27.555 109.544-109.544-109.544-109.544 27.555-27.555 109.544 109.544 109.544-109.544 27.555 27.555-109.544 109.544 109.713 109.544z" /> | ||||
| <glyph unicode="" glyph-name="paste-enabled" data-tags="paste-enabled" d="M410.237 57.275c-75.402 0-136.793 61.394-136.793 136.793v373.025c0 75.402 61.394 136.793 136.793 136.793h64.85l4.152 11.413c15.219 41.678 47.732 65.715 89.237 65.715 42.716 0 78.512-25.075 93.212-65.715l4.152-11.413h64.85c37.007 0 73.499-15.911 99.786-43.581 25.594-26.805 38.737-61.048 37.007-96.326v-369.738c0-75.402-61.394-136.793-136.793-136.793l-320.453-0.173zM360.947 638.689c-24.729-15.046-40.64-44.619-40.64-75.575v-373.025c0-49.804 40.467-90.271 90.271-90.271h324.432c43.754 0 80.415 31.476 88.545 72.981h1.73l0.173 17.295v373.025c0 28.708-14.181 58.627-35.277 74.71l-27.67 20.923v-17.468c0-47.213-36.834-84.048-84.048-84.048h-207.351c-47.213 0-84.048 36.834-84.048 84.048v13.489l-26.113-16.084zM572.451 733.631c-27.843 0-48.766-20.923-48.766-48.766 0-26.459 22.307-48.766 48.766-48.766s48.766 22.307 48.766 48.766c0.173 26.286-22.134 48.766-48.766 48.766zM422.169 764.069c7.78 8.818 16.603 16.776 24.383 25.594 1.903 2.076 3.806 4.325 5.709 6.401h-158.585c-75.748 0-137.312-61.567-137.312-137.312v-374.236c0-44.965 21.788-84.913 55.167-109.988v68.829c-5.536 12.278-8.472 26.113-8.472 41.159v374.236c0 49.804 40.64 90.444 90.444 90.444h116.561c3.633 5.19 7.78 10.202 12.105 14.873z" /> | ||||
| <glyph unicode="" glyph-name="paste-disabled" data-tags="paste-disabled" d="M410.237 57.275c-75.402 0-136.793 61.394-136.793 136.793v373.025c0 75.402 61.394 136.793 136.793 136.793h64.85l4.152 11.413c15.219 41.678 47.732 65.715 89.237 65.715 42.716 0 78.512-25.075 93.212-65.715l4.152-11.413h64.85c37.007 0 73.499-15.911 99.786-43.581 25.594-26.805 38.737-61.048 37.007-96.326v-369.738c0-75.402-61.394-136.793-136.793-136.793l-320.453-0.173zM360.947 638.689c-24.729-15.046-40.64-44.619-40.64-75.575v-373.025c0-49.804 40.467-90.271 90.271-90.271h324.432c43.754 0 80.415 31.476 88.545 72.981h1.73l0.173 17.295v373.025c0 28.708-14.181 58.627-35.277 74.71l-27.67 20.923v-17.468c0-47.213-36.834-84.048-84.048-84.048h-207.351c-47.213 0-84.048 36.834-84.048 84.048v13.489l-26.113-16.084zM572.451 733.631c-27.843 0-48.766-20.923-48.766-48.766 0-26.459 22.307-48.766 48.766-48.766s48.766 22.307 48.766 48.766c0.173 26.286-22.134 48.766-48.766 48.766zM422.169 764.069c7.78 8.818 16.603 16.776 24.383 25.594 1.903 2.076 3.806 4.325 5.709 6.401h-158.585c-75.748 0-137.312-61.567-137.312-137.312v-374.236c0-44.965 21.788-84.913 55.167-109.988v68.829c-5.536 12.278-8.472 26.113-8.472 41.159v374.236c0 49.804 40.64 90.444 90.444 90.444h116.561c3.633 5.19 7.78 10.202 12.105 14.873zM718.585 463.848l-28.016 27.843-110.68-110.68-110.68 110.68-27.843-27.843 110.68-110.68-110.68-110.68 27.843-27.843 110.68 110.68 110.68-110.68 27.843 27.843-110.68 110.68 110.853 110.68z" /> | ||||
| <glyph unicode="" glyph-name="button-disabled" data-tags="button-disabled" d="M853.333 699.246l-68.754 68.754-272.579-272.579-272.579 272.579-68.754-68.754 272.579-272.579-272.579-272.579 68.754-68.754 272.579 272.579 272.579-272.579 68.754 68.754-272.579 272.579z" /> | ||||
| </font></defs></svg> | ||||
| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/core/features/h5p/assets/fonts/h5p-core-23.woff
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										100
									
								
								src/core/features/h5p/assets/js/h5p-action-bar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/core/features/h5p/assets/js/h5p-action-bar.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| /** | ||||
|  * @class | ||||
|  * @augments H5P.EventDispatcher | ||||
|  * @param {Object} displayOptions | ||||
|  * @param {boolean} displayOptions.export Triggers the display of the 'Download' button | ||||
|  * @param {boolean} displayOptions.copyright Triggers the display of the 'Copyright' button | ||||
|  * @param {boolean} displayOptions.embed Triggers the display of the 'Embed' button | ||||
|  * @param {boolean} displayOptions.icon Triggers the display of the 'H5P icon' link | ||||
|  */ | ||||
| H5P.ActionBar = (function ($, EventDispatcher) { | ||||
|   "use strict"; | ||||
| 
 | ||||
|   function ActionBar(displayOptions) { | ||||
|     EventDispatcher.call(this); | ||||
| 
 | ||||
|     /** @alias H5P.ActionBar# */ | ||||
|     var self = this; | ||||
| 
 | ||||
|     var hasActions = false; | ||||
| 
 | ||||
|     // Create action bar
 | ||||
|     var $actions = H5P.jQuery('<ul class="h5p-actions"></ul>'); | ||||
| 
 | ||||
|     /** | ||||
|      * Helper for creating action bar buttons. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {string} type | ||||
|      * @param {string} customClass Instead of type class | ||||
|      */ | ||||
|     var addActionButton = function (type, customClass) { | ||||
|       /** | ||||
|        * Handles selection of action | ||||
|        */ | ||||
|       var handler = function () { | ||||
|         self.trigger(type); | ||||
|       }; | ||||
|       H5P.jQuery('<li/>', { | ||||
|         'class': 'h5p-button h5p-noselect h5p-' + (customClass ? customClass : type), | ||||
|         role: 'button', | ||||
|         tabindex: 0, | ||||
|         title: H5P.t(type + 'Description'), | ||||
|         html: H5P.t(type), | ||||
|         on: { | ||||
|           click: handler, | ||||
|           keypress: function (e) { | ||||
|             if (e.which === 32) { | ||||
|               handler(); | ||||
|               e.preventDefault(); // (since return false will block other inputs)
 | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         appendTo: $actions | ||||
|       }); | ||||
| 
 | ||||
|       hasActions = true; | ||||
|     }; | ||||
| 
 | ||||
|     // Register action bar buttons
 | ||||
|     if (displayOptions.export || displayOptions.copy) { | ||||
|       // Add export button
 | ||||
|       addActionButton('reuse', 'export'); | ||||
|     } | ||||
|     if (displayOptions.copyright) { | ||||
|       addActionButton('copyrights'); | ||||
|     } | ||||
|     if (displayOptions.embed) { | ||||
|       addActionButton('embed'); | ||||
|     } | ||||
|     if (displayOptions.icon) { | ||||
|       // Add about H5P button icon
 | ||||
|       H5P.jQuery('<li><a class="h5p-link" href="http://h5p.org" target="_blank" title="' + H5P.t('h5pDescription') + '"></a></li>').appendTo($actions); | ||||
|       hasActions = true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a reference to the dom element | ||||
|      * | ||||
|      * @return {H5P.jQuery} | ||||
|      */ | ||||
|     self.getDOMElement = function () { | ||||
|       return $actions; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Does the actionbar contain actions? | ||||
|      * | ||||
|      * @return {Boolean} | ||||
|      */ | ||||
|     self.hasActions = function () { | ||||
|       return hasActions; | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   ActionBar.prototype = Object.create(EventDispatcher.prototype); | ||||
|   ActionBar.prototype.constructor = ActionBar; | ||||
| 
 | ||||
|   return ActionBar; | ||||
| 
 | ||||
| })(H5P.jQuery, H5P.EventDispatcher); | ||||
							
								
								
									
										410
									
								
								src/core/features/h5p/assets/js/h5p-confirmation-dialog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								src/core/features/h5p/assets/js/h5p-confirmation-dialog.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,410 @@ | ||||
| /*global H5P*/ | ||||
| H5P.ConfirmationDialog = (function (EventDispatcher) { | ||||
|   "use strict"; | ||||
| 
 | ||||
|   /** | ||||
|    * Create a confirmation dialog | ||||
|    * | ||||
|    * @param [options] Options for confirmation dialog | ||||
|    * @param [options.instance] Instance that uses confirmation dialog | ||||
|    * @param [options.headerText] Header text | ||||
|    * @param [options.dialogText] Dialog text | ||||
|    * @param [options.cancelText] Cancel dialog button text | ||||
|    * @param [options.confirmText] Confirm dialog button text | ||||
|    * @param [options.hideCancel] Hide cancel button | ||||
|    * @param [options.hideExit] Hide exit button | ||||
|    * @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog | ||||
|    * @param [options.classes] Extra classes for popup | ||||
|    * @constructor | ||||
|    */ | ||||
|   function ConfirmationDialog(options) { | ||||
|     EventDispatcher.call(this); | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Make sure confirmation dialogs have unique id
 | ||||
|     H5P.ConfirmationDialog.uniqueId += 1; | ||||
|     var uniqueId = H5P.ConfirmationDialog.uniqueId; | ||||
| 
 | ||||
|     // Default options
 | ||||
|     options = options || {}; | ||||
|     options.headerText = options.headerText || H5P.t('confirmDialogHeader'); | ||||
|     options.dialogText = options.dialogText || H5P.t('confirmDialogBody'); | ||||
|     options.cancelText = options.cancelText || H5P.t('cancelLabel'); | ||||
|     options.confirmText = options.confirmText || H5P.t('confirmLabel'); | ||||
| 
 | ||||
|     /** | ||||
|      * Handle confirming event | ||||
|      * @param {Event} e | ||||
|      */ | ||||
|     function dialogConfirmed(e) { | ||||
|       self.hide(); | ||||
|       self.trigger('confirmed'); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle dialog canceled | ||||
|      * @param {Event} e | ||||
|      */ | ||||
|     function dialogCanceled(e) { | ||||
|       self.hide(); | ||||
|       self.trigger('canceled'); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Flow focus to element | ||||
|      * @param {HTMLElement} element Next element to be focused | ||||
|      * @param {Event} e Original tab event | ||||
|      */ | ||||
|     function flowTo(element, e) { | ||||
|       element.focus(); | ||||
|       e.preventDefault(); | ||||
|     } | ||||
| 
 | ||||
|     // Offset of exit button
 | ||||
|     var exitButtonOffset = 2 * 16; | ||||
|     var shadowOffset = 8; | ||||
| 
 | ||||
|     // Determine if we are too large for our container and must resize
 | ||||
|     var resizeIFrame = false; | ||||
| 
 | ||||
|     // Create background
 | ||||
|     var popupBackground = document.createElement('div'); | ||||
|     popupBackground.classList | ||||
|       .add('h5p-confirmation-dialog-background', 'hidden', 'hiding'); | ||||
| 
 | ||||
|     // Create outer popup
 | ||||
|     var popup = document.createElement('div'); | ||||
|     popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); | ||||
|     if (options.classes) { | ||||
|       options.classes.forEach(function (popupClass) { | ||||
|         popup.classList.add(popupClass); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     popup.setAttribute('role', 'dialog'); | ||||
|     popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId); | ||||
|     popupBackground.appendChild(popup); | ||||
|     popup.addEventListener('keydown', function (e) { | ||||
|       if (e.which === 27) {// Esc key
 | ||||
|         // Exit dialog
 | ||||
|         dialogCanceled(e); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     // Popup header
 | ||||
|     var header = document.createElement('div'); | ||||
|     header.classList.add('h5p-confirmation-dialog-header'); | ||||
|     popup.appendChild(header); | ||||
| 
 | ||||
|     // Header text
 | ||||
|     var headerText = document.createElement('div'); | ||||
|     headerText.classList.add('h5p-confirmation-dialog-header-text'); | ||||
|     headerText.innerHTML = options.headerText; | ||||
|     header.appendChild(headerText); | ||||
| 
 | ||||
|     // Popup body
 | ||||
|     var body = document.createElement('div'); | ||||
|     body.classList.add('h5p-confirmation-dialog-body'); | ||||
|     popup.appendChild(body); | ||||
| 
 | ||||
|     // Popup text
 | ||||
|     var text = document.createElement('div'); | ||||
|     text.classList.add('h5p-confirmation-dialog-text'); | ||||
|     text.innerHTML = options.dialogText; | ||||
|     text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId; | ||||
|     body.appendChild(text); | ||||
| 
 | ||||
|     // Popup buttons
 | ||||
|     var buttons = document.createElement('div'); | ||||
|     buttons.classList.add('h5p-confirmation-dialog-buttons'); | ||||
|     body.appendChild(buttons); | ||||
| 
 | ||||
|     // Cancel button
 | ||||
|     var cancelButton = document.createElement('button'); | ||||
|     cancelButton.classList.add('h5p-core-cancel-button'); | ||||
|     cancelButton.textContent = options.cancelText; | ||||
| 
 | ||||
|     // Confirm button
 | ||||
|     var confirmButton = document.createElement('button'); | ||||
|     confirmButton.classList.add('h5p-core-button'); | ||||
|     confirmButton.classList.add('h5p-confirmation-dialog-confirm-button'); | ||||
|     confirmButton.textContent = options.confirmText; | ||||
| 
 | ||||
|     // Exit button
 | ||||
|     var exitButton = document.createElement('button'); | ||||
|     exitButton.classList.add('h5p-confirmation-dialog-exit'); | ||||
|     exitButton.setAttribute('aria-hidden', 'true'); | ||||
|     exitButton.tabIndex = -1; | ||||
|     exitButton.title = options.cancelText; | ||||
| 
 | ||||
|     // Cancel handler
 | ||||
|     cancelButton.addEventListener('click', dialogCanceled); | ||||
|     cancelButton.addEventListener('keydown', function (e) { | ||||
|       if (e.which === 32) { // Space
 | ||||
|         dialogCanceled(e); | ||||
|       } | ||||
|       else if (e.which === 9 && e.shiftKey) { // Shift-tab
 | ||||
|         flowTo(confirmButton, e); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (!options.hideCancel) { | ||||
|       buttons.appendChild(cancelButton); | ||||
|     } | ||||
|     else { | ||||
|       // Center buttons
 | ||||
|       buttons.classList.add('center'); | ||||
|     } | ||||
| 
 | ||||
|     // Confirm handler
 | ||||
|     confirmButton.addEventListener('click', dialogConfirmed); | ||||
|     confirmButton.addEventListener('keydown', function (e) { | ||||
|       if (e.which === 32) { // Space
 | ||||
|         dialogConfirmed(e); | ||||
|       } | ||||
|       else if (e.which === 9 && !e.shiftKey) { // Tab
 | ||||
|         const nextButton = !options.hideCancel ? cancelButton : confirmButton; | ||||
|         flowTo(nextButton, e); | ||||
|       } | ||||
|     }); | ||||
|     buttons.appendChild(confirmButton); | ||||
| 
 | ||||
|     // Exit handler
 | ||||
|     exitButton.addEventListener('click', dialogCanceled); | ||||
|     exitButton.addEventListener('keydown', function (e) { | ||||
|       if (e.which === 32) { // Space
 | ||||
|         dialogCanceled(e); | ||||
|       } | ||||
|     }); | ||||
|     if (!options.hideExit) { | ||||
|       popup.appendChild(exitButton); | ||||
|     } | ||||
| 
 | ||||
|     // Wrapper element
 | ||||
|     var wrapperElement; | ||||
| 
 | ||||
|     // Focus capturing
 | ||||
|     var focusPredator; | ||||
| 
 | ||||
|     // Maintains hidden state of elements
 | ||||
|     var wrapperSiblingsHidden = []; | ||||
|     var popupSiblingsHidden = []; | ||||
| 
 | ||||
|     // Element with focus before dialog
 | ||||
|     var previouslyFocused; | ||||
| 
 | ||||
|     /** | ||||
|      * Set parent of confirmation dialog | ||||
|      * @param {HTMLElement} wrapper | ||||
|      * @returns {H5P.ConfirmationDialog} | ||||
|      */ | ||||
|     this.appendTo = function (wrapper) { | ||||
|       wrapperElement = wrapper; | ||||
|       return this; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Capture the focus element, send it to confirmation button | ||||
|      * @param {Event} e Original focus event | ||||
|      */ | ||||
|     var captureFocus = function (e) { | ||||
|       if (!popupBackground.contains(e.target)) { | ||||
|         e.preventDefault(); | ||||
|         confirmButton.focus(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Hide siblings of element from assistive technology | ||||
|      * | ||||
|      * @param {HTMLElement} element | ||||
|      * @returns {Array} The previous hidden state of all siblings | ||||
|      */ | ||||
|     var hideSiblings = function (element) { | ||||
|       var hiddenSiblings = []; | ||||
|       var siblings = element.parentNode.children; | ||||
|       var i; | ||||
|       for (i = 0; i < siblings.length; i += 1) { | ||||
|         // Preserve hidden state
 | ||||
|         hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ? | ||||
|           true : false; | ||||
| 
 | ||||
|         if (siblings[i] !== element) { | ||||
|           siblings[i].setAttribute('aria-hidden', true); | ||||
|         } | ||||
|       } | ||||
|       return hiddenSiblings; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Restores assistive technology state of element's siblings | ||||
|      * | ||||
|      * @param {HTMLElement} element | ||||
|      * @param {Array} hiddenSiblings Hidden state of all siblings | ||||
|      */ | ||||
|     var restoreSiblings = function (element, hiddenSiblings) { | ||||
|       var siblings = element.parentNode.children; | ||||
|       var i; | ||||
|       for (i = 0; i < siblings.length; i += 1) { | ||||
|         if (siblings[i] !== element && !hiddenSiblings[i]) { | ||||
|           siblings[i].removeAttribute('aria-hidden'); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Start capturing focus of parent and send it to dialog | ||||
|      */ | ||||
|     var startCapturingFocus = function () { | ||||
|       focusPredator = wrapperElement.parentNode || wrapperElement; | ||||
|       focusPredator.addEventListener('focus', captureFocus, true); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Clean up event listener for capturing focus | ||||
|      */ | ||||
|     var stopCapturingFocus = function () { | ||||
|       focusPredator.removeAttribute('aria-hidden'); | ||||
|       focusPredator.removeEventListener('focus', captureFocus, true); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Hide siblings in underlay from assistive technologies | ||||
|      */ | ||||
|     var disableUnderlay = function () { | ||||
|       wrapperSiblingsHidden = hideSiblings(wrapperElement); | ||||
|       popupSiblingsHidden = hideSiblings(popupBackground); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Restore state of underlay for assistive technologies | ||||
|      */ | ||||
|     var restoreUnderlay = function () { | ||||
|       restoreSiblings(wrapperElement, wrapperSiblingsHidden); | ||||
|       restoreSiblings(popupBackground, popupSiblingsHidden); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Fit popup to container. Makes sure it doesn't overflow. | ||||
|      * @params {number} [offsetTop] Offset of popup | ||||
|      */ | ||||
|     var fitToContainer = function (offsetTop) { | ||||
|       var popupOffsetTop = parseInt(popup.style.top, 10); | ||||
|       if (offsetTop !== undefined) { | ||||
|         popupOffsetTop = offsetTop; | ||||
|       } | ||||
| 
 | ||||
|       if (!popupOffsetTop) { | ||||
|         popupOffsetTop = 0; | ||||
|       } | ||||
| 
 | ||||
|       // Overflows height
 | ||||
|       if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) { | ||||
|         popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset; | ||||
|       } | ||||
| 
 | ||||
|       if (popupOffsetTop - exitButtonOffset <= 0) { | ||||
|         popupOffsetTop = exitButtonOffset + shadowOffset; | ||||
| 
 | ||||
|         // We are too big and must resize
 | ||||
|         resizeIFrame = true; | ||||
|       } | ||||
|       popup.style.top = popupOffsetTop + 'px'; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Show confirmation dialog | ||||
|      * @params {number} offsetTop Offset top | ||||
|      * @returns {H5P.ConfirmationDialog} | ||||
|      */ | ||||
|     this.show = function (offsetTop) { | ||||
|       // Capture focused item
 | ||||
|       previouslyFocused = document.activeElement; | ||||
|       wrapperElement.appendChild(popupBackground); | ||||
|       startCapturingFocus(); | ||||
|       disableUnderlay(); | ||||
|       popupBackground.classList.remove('hidden'); | ||||
|       fitToContainer(offsetTop); | ||||
|       setTimeout(function () { | ||||
|         popup.classList.remove('hidden'); | ||||
|         popupBackground.classList.remove('hiding'); | ||||
| 
 | ||||
|         setTimeout(function () { | ||||
|           // Focus confirm button
 | ||||
|           confirmButton.focus(); | ||||
| 
 | ||||
|           // Resize iFrame if necessary
 | ||||
|           if (resizeIFrame && options.instance) { | ||||
|             var minHeight = parseInt(popup.offsetHeight, 10) + | ||||
|               exitButtonOffset + (2 * shadowOffset); | ||||
|             self.setViewPortMinimumHeight(minHeight); | ||||
|             options.instance.trigger('resize'); | ||||
|             resizeIFrame = false; | ||||
|           } | ||||
|         }, 100); | ||||
|       }, 0); | ||||
| 
 | ||||
|       return this; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Hide confirmation dialog | ||||
|      * @returns {H5P.ConfirmationDialog} | ||||
|      */ | ||||
|     this.hide = function () { | ||||
|       popupBackground.classList.add('hiding'); | ||||
|       popup.classList.add('hidden'); | ||||
| 
 | ||||
|       // Restore focus
 | ||||
|       stopCapturingFocus(); | ||||
|       if (!options.skipRestoreFocus) { | ||||
|         previouslyFocused.focus(); | ||||
|       } | ||||
|       restoreUnderlay(); | ||||
|       setTimeout(function () { | ||||
|         popupBackground.classList.add('hidden'); | ||||
|         wrapperElement.removeChild(popupBackground); | ||||
|         self.setViewPortMinimumHeight(null); | ||||
|       }, 100); | ||||
| 
 | ||||
|       return this; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve element | ||||
|      * | ||||
|      * @return {HTMLElement} | ||||
|      */ | ||||
|     this.getElement = function () { | ||||
|       return popup; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Get previously focused element | ||||
|      * @return {HTMLElement} | ||||
|      */ | ||||
|     this.getPreviouslyFocused = function () { | ||||
|       return previouslyFocused; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the minimum height of the view port | ||||
|      * | ||||
|      * @param {number|null} minHeight | ||||
|      */ | ||||
|     this.setViewPortMinimumHeight = function (minHeight) { | ||||
|       var container = document.querySelector('.h5p-container') || document.body; | ||||
|       container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight; | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype); | ||||
|   ConfirmationDialog.prototype.constructor = ConfirmationDialog; | ||||
| 
 | ||||
|   return ConfirmationDialog; | ||||
| 
 | ||||
| }(H5P.EventDispatcher)); | ||||
| 
 | ||||
| H5P.ConfirmationDialog.uniqueId = -1; | ||||
							
								
								
									
										41
									
								
								src/core/features/h5p/assets/js/h5p-content-type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/core/features/h5p/assets/js/h5p-content-type.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| /** | ||||
|  * H5P.ContentType is a base class for all content types. Used by newRunnable() | ||||
|  * | ||||
|  * Functions here may be overridable by the libraries. In special cases, | ||||
|  * it is also possible to override H5P.ContentType on a global level. | ||||
|  * | ||||
|  * NOTE that this doesn't actually 'extend' the event dispatcher but instead | ||||
|  * it creates a single instance which all content types shares as their base | ||||
|  * prototype. (in some cases this may be the root of strange event behavior) | ||||
|  * | ||||
|  * @class | ||||
|  * @augments H5P.EventDispatcher | ||||
|  */ | ||||
| H5P.ContentType = function (isRootLibrary) { | ||||
| 
 | ||||
|   function ContentType() {} | ||||
| 
 | ||||
|   // Inherit from EventDispatcher.
 | ||||
|   ContentType.prototype = new H5P.EventDispatcher(); | ||||
| 
 | ||||
|   /** | ||||
|    * Is library standalone or not? Not beeing standalone, means it is | ||||
|    * included in another library | ||||
|    * | ||||
|    * @return {Boolean} | ||||
|    */ | ||||
|   ContentType.prototype.isRoot = function () { | ||||
|     return isRootLibrary; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the file path of a file in the current library | ||||
|    * @param  {string} filePath The path to the file relative to the library folder | ||||
|    * @return {string} The full path to the file | ||||
|    */ | ||||
|   ContentType.prototype.getLibraryFilePath = function (filePath) { | ||||
|     return H5P.getLibraryPath(this.libraryInfo.versionedNameNoSpaces) + '/' + filePath; | ||||
|   }; | ||||
| 
 | ||||
|   return ContentType; | ||||
| }; | ||||
							
								
								
									
										313
									
								
								src/core/features/h5p/assets/js/h5p-content-upgrade-process.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								src/core/features/h5p/assets/js/h5p-content-upgrade-process.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,313 @@ | ||||
| /*jshint -W083 */ | ||||
| var H5PUpgrades = H5PUpgrades || {}; | ||||
| 
 | ||||
| H5P.ContentUpgradeProcess = (function (Version) { | ||||
| 
 | ||||
|   /** | ||||
|    * @class | ||||
|    * @namespace H5P | ||||
|    */ | ||||
|   function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Make params possible to work with
 | ||||
|     try { | ||||
|       params = JSON.parse(params); | ||||
|       if (!(params instanceof Object)) { | ||||
|         throw true; | ||||
|       } | ||||
|     } | ||||
|     catch (event) { | ||||
|       return done({ | ||||
|         type: 'errorParamsBroken', | ||||
|         id: id | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     self.loadLibrary = loadLibrary; | ||||
|     self.upgrade(name, oldVersion, newVersion, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { | ||||
|       if (err) { | ||||
|         err.id = id; | ||||
|         return done(err); | ||||
|       } | ||||
| 
 | ||||
|       done(null, JSON.stringify({params: upgradedParams, metadata: upgradedMetadata})); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Run content upgrade. | ||||
|    * | ||||
|    * @public | ||||
|    * @param {string} name | ||||
|    * @param {Version} oldVersion | ||||
|    * @param {Version} newVersion | ||||
|    * @param {Object} params | ||||
|    * @param {Object} metadata | ||||
|    * @param {Function} done | ||||
|    */ | ||||
|   ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, metadata, done) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Load library details and upgrade routines
 | ||||
|     self.loadLibrary(name, newVersion, function (err, library) { | ||||
|       if (err) { | ||||
|         return done(err); | ||||
|       } | ||||
|       if (library.semantics === null) { | ||||
|         return done({ | ||||
|           type: 'libraryMissing', | ||||
|           library: library.name + ' ' + library.version.major + '.' + library.version.minor | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       // Run upgrade routines on params
 | ||||
|       self.processParams(library, oldVersion, newVersion, params, metadata, function (err, params, metadata) { | ||||
|         if (err) { | ||||
|           return done(err); | ||||
|         } | ||||
| 
 | ||||
|         // Check if any of the sub-libraries need upgrading
 | ||||
|         asyncSerial(library.semantics, function (index, field, next) { | ||||
|           self.processField(field, params[field.name], function (err, upgradedParams) { | ||||
|             if (upgradedParams) { | ||||
|               params[field.name] = upgradedParams; | ||||
|             } | ||||
|             next(err); | ||||
|           }); | ||||
|         }, function (err) { | ||||
|           done(err, params, metadata); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Run upgrade hooks on params. | ||||
|    * | ||||
|    * @public | ||||
|    * @param {Object} library | ||||
|    * @param {Version} oldVersion | ||||
|    * @param {Version} newVersion | ||||
|    * @param {Object} params | ||||
|    * @param {Function} next | ||||
|    */ | ||||
|   ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, metadata, next) { | ||||
|     if (H5PUpgrades[library.name] === undefined) { | ||||
|       if (library.upgradesScript) { | ||||
|         // Upgrades script should be loaded so the upgrades should be here.
 | ||||
|         return next({ | ||||
|           type: 'scriptMissing', | ||||
|           library: library.name + ' ' + newVersion | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       // No upgrades script. Move on
 | ||||
|       return next(null, params, metadata); | ||||
|     } | ||||
| 
 | ||||
|     // Run upgrade hooks. Start by going through major versions
 | ||||
|     asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) { | ||||
|       if (major < oldVersion.major || major > newVersion.major) { | ||||
|         // Older than the current version or newer than the selected
 | ||||
|         nextMajor(); | ||||
|       } | ||||
|       else { | ||||
|         // Go through the minor versions for this major version
 | ||||
|         asyncSerial(minors, function (minor, upgrade, nextMinor) { | ||||
|           minor =+ minor; | ||||
|           if (minor <= oldVersion.minor || minor > newVersion.minor) { | ||||
|             // Older than or equal to the current version or newer than the selected
 | ||||
|             nextMinor(); | ||||
|           } | ||||
|           else { | ||||
|             // We found an upgrade hook, run it
 | ||||
|             var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade); | ||||
| 
 | ||||
|             try { | ||||
|               unnecessaryWrapper(params, function (err, upgradedParams, upgradedExtras) { | ||||
|                 params = upgradedParams; | ||||
|                 if (upgradedExtras && upgradedExtras.metadata) { // Optional
 | ||||
|                   metadata = upgradedExtras.metadata; | ||||
|                 } | ||||
|                 nextMinor(err); | ||||
|               }, {metadata: metadata}); | ||||
|             } | ||||
|             catch (err) { | ||||
|               if (console && console.error) { | ||||
|                 console.error("Error", err.stack); | ||||
|                 console.error("Error", err.name); | ||||
|                 console.error("Error", err.message); | ||||
|               } | ||||
|               next(err); | ||||
|             } | ||||
|           } | ||||
|         }, nextMajor); | ||||
|       } | ||||
|     }, function (err) { | ||||
|       next(err, params, metadata); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Process parameter fields to find and upgrade sub-libraries. | ||||
|    * | ||||
|    * @public | ||||
|    * @param {Object} field | ||||
|    * @param {Object} params | ||||
|    * @param {Function} done | ||||
|    */ | ||||
|   ContentUpgradeProcess.prototype.processField = function (field, params, done) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (params === undefined) { | ||||
|       return done(); | ||||
|     } | ||||
| 
 | ||||
|     switch (field.type) { | ||||
|       case 'library': | ||||
|         if (params.library === undefined || params.params === undefined) { | ||||
|           return done(); | ||||
|         } | ||||
| 
 | ||||
|         // Look for available upgrades
 | ||||
|         var usedLib = params.library.split(' ', 2); | ||||
|         for (var i = 0; i < field.options.length; i++) { | ||||
|           var availableLib = (typeof field.options[i] === 'string') ? field.options[i].split(' ', 2) : field.options[i].name.split(' ', 2); | ||||
|           if (availableLib[0] === usedLib[0]) { | ||||
|             if (availableLib[1] === usedLib[1]) { | ||||
|               return done(); // Same version
 | ||||
|             } | ||||
| 
 | ||||
|             // We have different versions
 | ||||
|             var usedVer = new Version(usedLib[1]); | ||||
|             var availableVer = new Version(availableLib[1]); | ||||
|             if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) { | ||||
|               return done({ | ||||
|                 type: 'errorTooHighVersion', | ||||
|                 used: usedLib[0] + ' ' + usedVer, | ||||
|                 supported: availableLib[0] + ' ' + availableVer | ||||
|               }); // Larger or same version that's available
 | ||||
|             } | ||||
| 
 | ||||
|             // A newer version is available, upgrade params
 | ||||
|             return self.upgrade(availableLib[0], usedVer, availableVer, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { | ||||
|               if (!err) { | ||||
|                 params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor; | ||||
|                 params.params = upgradedParams; | ||||
|                 if (upgradedMetadata) { | ||||
|                   params.metadata = upgradedMetadata; | ||||
|                 } | ||||
|               } | ||||
|               done(err, params); | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         // Content type was not supporte by the higher version
 | ||||
|         done({ | ||||
|           type: 'errorNotSupported', | ||||
|           used: usedLib[0] + ' ' + usedVer | ||||
|         }); | ||||
|         break; | ||||
| 
 | ||||
|       case 'group': | ||||
|         if (field.fields.length === 1 && field.isSubContent !== true) { | ||||
|           // Single field to process, wrapper will be skipped
 | ||||
|           self.processField(field.fields[0], params, function (err, upgradedParams) { | ||||
|             if (upgradedParams) { | ||||
|               params = upgradedParams; | ||||
|             } | ||||
|             done(err, params); | ||||
|           }); | ||||
|         } | ||||
|         else { | ||||
|           // Go through all fields in the group
 | ||||
|           asyncSerial(field.fields, function (index, subField, next) { | ||||
|             var paramsToProcess = params ? params[subField.name] : null; | ||||
|             self.processField(subField, paramsToProcess, function (err, upgradedParams) { | ||||
|               if (upgradedParams) { | ||||
|                 params[subField.name] = upgradedParams; | ||||
|               } | ||||
|               next(err); | ||||
|             }); | ||||
| 
 | ||||
|           }, function (err) { | ||||
|             done(err, params); | ||||
|           }); | ||||
|         } | ||||
|         break; | ||||
| 
 | ||||
|       case 'list': | ||||
|         // Go trough all params in the list
 | ||||
|         asyncSerial(params, function (index, subParams, next) { | ||||
|           self.processField(field.field, subParams, function (err, upgradedParams) { | ||||
|             if (upgradedParams) { | ||||
|               params[index] = upgradedParams; | ||||
|             } | ||||
|             next(err); | ||||
|           }); | ||||
|         }, function (err) { | ||||
|           done(err, params); | ||||
|         }); | ||||
|         break; | ||||
| 
 | ||||
|       default: | ||||
|         done(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Helps process each property on the given object asynchronously in serial order. | ||||
|    * | ||||
|    * @private | ||||
|    * @param {Object} obj | ||||
|    * @param {Function} process | ||||
|    * @param {Function} finished | ||||
|    */ | ||||
|   var asyncSerial = function (obj, process, finished) { | ||||
|     var id, isArray = obj instanceof Array; | ||||
| 
 | ||||
|     // Keep track of each property that belongs to this object.
 | ||||
|     if (!isArray) { | ||||
|       var ids = []; | ||||
|       for (id in obj) { | ||||
|         if (obj.hasOwnProperty(id)) { | ||||
|           ids.push(id); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var i = -1; // Keeps track of the current property
 | ||||
| 
 | ||||
|     /** | ||||
|      * Private. Process the next property | ||||
|      */ | ||||
|     var next = function () { | ||||
|       id = isArray ? i : ids[i]; | ||||
|       process(id, obj[id], check); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Private. Check if we're done or have an error. | ||||
|      * | ||||
|      * @param {String} err | ||||
|      */ | ||||
|     var check = function (err) { | ||||
|       // We need to use a real async function in order for the stack to clear.
 | ||||
|       setTimeout(function () { | ||||
|         i++; | ||||
|         if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) { | ||||
|           finished(err); | ||||
|         } | ||||
|         else { | ||||
|           next(); | ||||
|         } | ||||
|       }, 0); | ||||
|     }; | ||||
| 
 | ||||
|     check(); // Start
 | ||||
|   }; | ||||
| 
 | ||||
|   return ContentUpgradeProcess; | ||||
| })(H5P.Version); | ||||
| @ -0,0 +1,63 @@ | ||||
| /* global importScripts */ | ||||
| var H5P = H5P || {}; | ||||
| importScripts('h5p-version.js', 'h5p-content-upgrade-process.js'); | ||||
| 
 | ||||
| var libraryLoadedCallback; | ||||
| 
 | ||||
| /** | ||||
|  * Register message handlers | ||||
|  */ | ||||
| var messageHandlers = { | ||||
|   newJob: function (job) { | ||||
|     // Start new job
 | ||||
|     new H5P.ContentUpgradeProcess(job.name, new H5P.Version(job.oldVersion), new H5P.Version(job.newVersion), job.params, job.id, function loadLibrary(name, version, next) { | ||||
|       // TODO: Cache?
 | ||||
|       postMessage({ | ||||
|         action: 'loadLibrary', | ||||
|         name: name, | ||||
|         version: version.toString() | ||||
|       }); | ||||
|       libraryLoadedCallback = next; | ||||
|     }, function done(err, result) { | ||||
|       if (err) { | ||||
|         // Return error
 | ||||
|         postMessage({ | ||||
|           action: 'error', | ||||
|           id: job.id, | ||||
|           err: err.message ? err.message : err | ||||
|         }); | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // Return upgraded content
 | ||||
|       postMessage({ | ||||
|         action: 'done', | ||||
|         id: job.id, | ||||
|         params: result | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|   libraryLoaded: function (data) { | ||||
|     var library = data.library; | ||||
|     if (library.upgradesScript) { | ||||
|       try { | ||||
|         importScripts(library.upgradesScript); | ||||
|       } | ||||
|       catch (err) { | ||||
|         libraryLoadedCallback(err); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     libraryLoadedCallback(null, data.library); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Handle messages from our master | ||||
|  */ | ||||
| onmessage = function (event) { | ||||
|   if (event.data.action !== undefined && messageHandlers[event.data.action]) { | ||||
|     messageHandlers[event.data.action].call(this, event.data); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										445
									
								
								src/core/features/h5p/assets/js/h5p-content-upgrade.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								src/core/features/h5p/assets/js/h5p-content-upgrade.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,445 @@ | ||||
| /* global H5PAdminIntegration H5PUtils */ | ||||
| 
 | ||||
| (function ($, Version) { | ||||
|   var info, $log, $container, librariesCache = {}, scriptsCache = {}; | ||||
| 
 | ||||
|   // Initialize
 | ||||
|   $(document).ready(function () { | ||||
|     // Get library info
 | ||||
|     info = H5PAdminIntegration.libraryInfo; | ||||
| 
 | ||||
|     // Get and reset container
 | ||||
|     const $wrapper = $('#h5p-admin-container').html(''); | ||||
|     $log = $('<ul class="content-upgrade-log"></ul>').appendTo($wrapper); | ||||
|     $container = $('<div><p>' + info.message + '</p></div>').appendTo($wrapper); | ||||
| 
 | ||||
|     // Make it possible to select version
 | ||||
|     var $version = $(getVersionSelect(info.versions)).appendTo($container); | ||||
| 
 | ||||
|     // Add "go" button
 | ||||
|     $('<button/>', { | ||||
|       class: 'h5p-admin-upgrade-button', | ||||
|       text: info.buttonLabel, | ||||
|       click: function () { | ||||
|         // Start new content upgrade
 | ||||
|         new ContentUpgrade($version.val()); | ||||
|       } | ||||
|     }).appendTo($container); | ||||
|   }); | ||||
| 
 | ||||
|   /** | ||||
|    * Generate html for version select. | ||||
|    * | ||||
|    * @param {Object} versions | ||||
|    * @returns {String} | ||||
|    */ | ||||
|   var getVersionSelect = function (versions) { | ||||
|     var html = ''; | ||||
|     for (var id in versions) { | ||||
|       html += '<option value="' + id + '">' + versions[id] + '</option>'; | ||||
|     } | ||||
|     if (html !== '') { | ||||
|       html = '<select>' + html + '</select>'; | ||||
|       return html; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Displays a throbber in the status field. | ||||
|    * | ||||
|    * @param {String} msg | ||||
|    * @returns {_L1.Throbber} | ||||
|    */ | ||||
|   function Throbber(msg) { | ||||
|     var $throbber = H5PUtils.throbber(msg); | ||||
|     $container.html('').append($throbber); | ||||
| 
 | ||||
|     /** | ||||
|      * Makes it possible to set the progress. | ||||
|      * | ||||
|      * @param {String} progress | ||||
|      */ | ||||
|     this.setProgress = function (progress) { | ||||
|       $throbber.text(msg + ' ' + progress); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Start a new content upgrade. | ||||
|    * | ||||
|    * @param {Number} libraryId | ||||
|    * @returns {_L1.ContentUpgrade} | ||||
|    */ | ||||
|   function ContentUpgrade(libraryId) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Get selected version
 | ||||
|     self.version = new Version(info.versions[libraryId]); | ||||
|     self.version.libraryId = libraryId; | ||||
| 
 | ||||
|     // Create throbber with loading text and progress
 | ||||
|     self.throbber = new Throbber(info.inProgress.replace('%ver', self.version)); | ||||
| 
 | ||||
|     self.started = new Date().getTime(); | ||||
|     self.io = 0; | ||||
| 
 | ||||
|     // Track number of working
 | ||||
|     self.working = 0; | ||||
| 
 | ||||
|     var start = function () { | ||||
|       // Get the next batch
 | ||||
|       self.nextBatch({ | ||||
|         libraryId: libraryId, | ||||
|         token: info.token | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     if (window.Worker !== undefined) { | ||||
|       // Prepare our workers
 | ||||
|       self.initWorkers(); | ||||
|       start(); | ||||
|     } | ||||
|     else { | ||||
|       // No workers, do the job ourselves
 | ||||
|       self.loadScript(info.scriptBaseUrl + '/h5p-content-upgrade-process.js' + info.buster, start); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Initialize workers | ||||
|    */ | ||||
|   ContentUpgrade.prototype.initWorkers = function () { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Determine number of workers (defaults to 4)
 | ||||
|     var numWorkers = (window.navigator !== undefined && window.navigator.hardwareConcurrency ? window.navigator.hardwareConcurrency : 4); | ||||
|     self.workers = new Array(numWorkers); | ||||
| 
 | ||||
|     // Register message handlers
 | ||||
|     var messageHandlers = { | ||||
|       done: function (result) { | ||||
|         self.workDone(result.id, result.params, this); | ||||
|       }, | ||||
|       error: function (error) { | ||||
|         self.printError(error.err); | ||||
|         self.workDone(error.id, null, this); | ||||
|       }, | ||||
|       loadLibrary: function (details) { | ||||
|         var worker = this; | ||||
|         self.loadLibrary(details.name, new Version(details.version), function (err, library) { | ||||
|           if (err) { | ||||
|             // Reset worker?
 | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           worker.postMessage({ | ||||
|             action: 'libraryLoaded', | ||||
|             library: library | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     for (var i = 0; i < numWorkers; i++) { | ||||
|       self.workers[i] = new Worker(info.scriptBaseUrl + '/h5p-content-upgrade-worker.js' + info.buster); | ||||
|       self.workers[i].onmessage = function (event) { | ||||
|         if (event.data.action !== undefined && messageHandlers[event.data.action]) { | ||||
|           messageHandlers[event.data.action].call(this, event.data); | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Get the next batch and start processing it. | ||||
|    * | ||||
|    * @param {Object} outData | ||||
|    */ | ||||
|   ContentUpgrade.prototype.nextBatch = function (outData) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Track time spent on IO
 | ||||
|     var start = new Date().getTime(); | ||||
|     $.post(info.infoUrl, outData, function (inData) { | ||||
|       self.io += new Date().getTime() - start; | ||||
|       if (!(inData instanceof Object)) { | ||||
|         // Print errors from backend
 | ||||
|         return self.setStatus(inData); | ||||
|       } | ||||
|       if (inData.left === 0) { | ||||
|         var total = new Date().getTime() - self.started; | ||||
| 
 | ||||
|         if (window.console && console.log) { | ||||
|           console.log('The upgrade process took ' + (total / 1000) + ' seconds. (' + (Math.round((self.io / (total / 100)) * 100) / 100) + ' % IO)' ); | ||||
|         } | ||||
| 
 | ||||
|         // Terminate workers
 | ||||
|         self.terminate(); | ||||
| 
 | ||||
|         // Nothing left to process
 | ||||
|         return self.setStatus(info.done); | ||||
|       } | ||||
| 
 | ||||
|       self.left = inData.left; | ||||
|       self.token = inData.token; | ||||
| 
 | ||||
|       // Start processing
 | ||||
|       self.processBatch(inData.params, inData.skipped); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Set current status message. | ||||
|    * | ||||
|    * @param {String} msg | ||||
|    */ | ||||
|   ContentUpgrade.prototype.setStatus = function (msg) { | ||||
|     $container.html(msg); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Process the given parameters. | ||||
|    * | ||||
|    * @param {Object} parameters | ||||
|    */ | ||||
|   ContentUpgrade.prototype.processBatch = function (parameters, skipped) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Track upgraded params
 | ||||
|     self.upgraded = {}; | ||||
|     self.skipped = skipped; | ||||
| 
 | ||||
|     // Track current batch
 | ||||
|     self.parameters = parameters; | ||||
| 
 | ||||
|     // Create id mapping
 | ||||
|     self.ids = []; | ||||
|     for (var id in parameters) { | ||||
|       if (parameters.hasOwnProperty(id)) { | ||||
|         self.ids.push(id); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Keep track of current content
 | ||||
|     self.current = -1; | ||||
| 
 | ||||
|     if (self.workers !== undefined) { | ||||
|       // Assign each worker content to upgrade
 | ||||
|       for (var i = 0; i < self.workers.length; i++) { | ||||
|         self.assignWork(self.workers[i]); | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
| 
 | ||||
|       self.assignWork(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    */ | ||||
|   ContentUpgrade.prototype.assignWork = function (worker) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     var id = self.ids[self.current + 1]; | ||||
|     if (id === undefined) { | ||||
|       return false; // Out of work
 | ||||
|     } | ||||
|     self.current++; | ||||
|     self.working++; | ||||
| 
 | ||||
|     if (worker) { | ||||
|       worker.postMessage({ | ||||
|         action: 'newJob', | ||||
|         id: id, | ||||
|         name: info.library.name, | ||||
|         oldVersion: info.library.version, | ||||
|         newVersion: self.version.toString(), | ||||
|         params: self.parameters[id] | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       new H5P.ContentUpgradeProcess(info.library.name, new Version(info.library.version), self.version, self.parameters[id], id, function loadLibrary(name, version, next) { | ||||
|         self.loadLibrary(name, version, function (err, library) { | ||||
|           if (library.upgradesScript) { | ||||
|             self.loadScript(library.upgradesScript, function (err) { | ||||
|               if (err) { | ||||
|                 err = info.errorScript.replace('%lib', name + ' ' + version); | ||||
|               } | ||||
|               next(err, library); | ||||
|             }); | ||||
|           } | ||||
|           else { | ||||
|             next(null, library); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|       }, function done(err, result) { | ||||
|         if (err) { | ||||
|           self.printError(err); | ||||
|           result = null; | ||||
|         } | ||||
| 
 | ||||
|         self.workDone(id, result); | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    */ | ||||
|   ContentUpgrade.prototype.workDone = function (id, result, worker) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     self.working--; | ||||
|     if (result === null) { | ||||
|       self.skipped.push(id); | ||||
|     } | ||||
|     else { | ||||
|       self.upgraded[id] = result; | ||||
|     } | ||||
| 
 | ||||
|     // Update progress message
 | ||||
|     self.throbber.setProgress(Math.round((info.total - self.left + self.current) / (info.total / 100)) + ' %'); | ||||
| 
 | ||||
|     // Assign next job
 | ||||
|     if (self.assignWork(worker) === false && self.working === 0) { | ||||
|       // All workers have finsihed.
 | ||||
|       self.nextBatch({ | ||||
|         libraryId: self.version.libraryId, | ||||
|         token: self.token, | ||||
|         skipped: JSON.stringify(self.skipped), | ||||
|         params: JSON.stringify(self.upgraded) | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    */ | ||||
|   ContentUpgrade.prototype.terminate = function () { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (self.workers) { | ||||
|       // Stop all workers
 | ||||
|       for (var i = 0; i < self.workers.length; i++) { | ||||
|         self.workers[i].terminate(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   var librariesLoadedCallbacks = {}; | ||||
| 
 | ||||
|   /** | ||||
|    * Load library data needed for content upgrade. | ||||
|    * | ||||
|    * @param {String} name | ||||
|    * @param {Version} version | ||||
|    * @param {Function} next | ||||
|    */ | ||||
|   ContentUpgrade.prototype.loadLibrary = function (name, version, next) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     var key = name + '/' + version.major + '/' + version.minor; | ||||
| 
 | ||||
|     if (librariesCache[key] === true) { | ||||
|       // Library is being loaded, que callback
 | ||||
|       if (librariesLoadedCallbacks[key] === undefined) { | ||||
|         librariesLoadedCallbacks[key] = [next]; | ||||
|         return; | ||||
|       } | ||||
|       librariesLoadedCallbacks[key].push(next); | ||||
|       return; | ||||
|     } | ||||
|     else if (librariesCache[key] !== undefined) { | ||||
|       // Library has been loaded before. Return cache.
 | ||||
|       next(null, librariesCache[key]); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Track time spent loading
 | ||||
|     var start = new Date().getTime(); | ||||
|     librariesCache[key] = true; | ||||
|     $.ajax({ | ||||
|       dataType: 'json', | ||||
|       cache: true, | ||||
|       url: info.libraryBaseUrl + '/' + key | ||||
|     }).fail(function () { | ||||
|       self.io += new Date().getTime() - start; | ||||
|       next(info.errorData.replace('%lib', name + ' ' + version)); | ||||
|     }).done(function (library) { | ||||
|       self.io += new Date().getTime() - start; | ||||
|       librariesCache[key] = library; | ||||
|       next(null, library); | ||||
| 
 | ||||
|       if (librariesLoadedCallbacks[key] !== undefined) { | ||||
|         for (var i = 0; i < librariesLoadedCallbacks[key].length; i++) { | ||||
|           librariesLoadedCallbacks[key][i](null, library); | ||||
|         } | ||||
|       } | ||||
|       delete librariesLoadedCallbacks[key]; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Load script with upgrade hooks. | ||||
|    * | ||||
|    * @param {String} url | ||||
|    * @param {Function} next | ||||
|    */ | ||||
|   ContentUpgrade.prototype.loadScript = function (url, next) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (scriptsCache[url] !== undefined) { | ||||
|       next(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Track time spent loading
 | ||||
|     var start = new Date().getTime(); | ||||
|     $.ajax({ | ||||
|       dataType: 'script', | ||||
|       cache: true, | ||||
|       url: url | ||||
|     }).fail(function () { | ||||
|       self.io += new Date().getTime() - start; | ||||
|       next(true); | ||||
|     }).done(function () { | ||||
|       scriptsCache[url] = true; | ||||
|       self.io += new Date().getTime() - start; | ||||
|       next(); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    */ | ||||
|   ContentUpgrade.prototype.printError = function (error) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     switch (error.type) { | ||||
|       case 'errorParamsBroken': | ||||
|         error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; | ||||
|         break; | ||||
| 
 | ||||
|       case 'libraryMissing': | ||||
|         error = info.errorLibrary.replace('%lib', error.library); | ||||
|         break; | ||||
| 
 | ||||
|       case 'scriptMissing': | ||||
|         error = info.errorScript.replace('%lib', error.library); | ||||
|         break; | ||||
| 
 | ||||
|       case 'errorTooHighVersion': | ||||
|         error = info.errorContent.replace('%id', error.id) + ' ' + info.errorTooHighVersion.replace('%used', error.used).replace('%supported', error.supported); | ||||
|         break; | ||||
| 
 | ||||
|       case 'errorNotSupported': | ||||
|         error = info.errorContent.replace('%id', error.id) + ' ' + info.errorNotSupported.replace('%used', error.used); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     $('<li>' + info.error + '<br/>' + error + '</li>').appendTo($log); | ||||
|   }; | ||||
| 
 | ||||
| })(H5P.jQuery, H5P.Version); | ||||
							
								
								
									
										442
									
								
								src/core/features/h5p/assets/js/h5p-data-view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										442
									
								
								src/core/features/h5p/assets/js/h5p-data-view.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,442 @@ | ||||
| /* global H5PUtils */ | ||||
| var H5PDataView = (function ($) { | ||||
| 
 | ||||
|   /** | ||||
|    * Initialize a new H5P data view. | ||||
|    * | ||||
|    * @class | ||||
|    * @param {Object} container | ||||
|    *   Element to clear out and append to. | ||||
|    * @param {String} source | ||||
|    *   URL to get data from. Data format: {num: 123, rows:[[1,2,3],[2,4,6]]} | ||||
|    * @param {Array} headers | ||||
|    *   List with column headers. Can be strings or objects with options like | ||||
|    *   "text" and "sortable". E.g. | ||||
|    *   [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] | ||||
|    * @param {Object} l10n | ||||
|    *   Localization / translations. e.g. | ||||
|    *   { | ||||
|    *     loading: 'Loading data.', | ||||
|    *     ajaxFailed: 'Failed to load data.', | ||||
|    *     noData: "There's no data available that matches your criteria.", | ||||
|    *     currentPage: 'Page $current of $total', | ||||
|    *     nextPage: 'Next page', | ||||
|    *     previousPage: 'Previous page', | ||||
|    *     search: 'Search' | ||||
|    *   } | ||||
|    * @param {Object} classes | ||||
|    *   Custom html classes to use on elements. | ||||
|    *   e.g. {tableClass: 'fixed'}. | ||||
|    * @param {Array} filters | ||||
|    *   Make it possible to filter/search in the given column. | ||||
|    *   e.g. [null, true, null, null] will make it possible to do a text | ||||
|    *   search in column 2. | ||||
|    * @param {Function} loaded | ||||
|    *   Callback for when data has been loaded. | ||||
|    * @param {Object} order | ||||
|    */ | ||||
|   function H5PDataView(container, source, headers, l10n, classes, filters, loaded, order) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     self.$container = $(container).addClass('h5p-data-view').html(''); | ||||
| 
 | ||||
|     self.source = source; | ||||
|     self.headers = headers; | ||||
|     self.l10n = l10n; | ||||
|     self.classes = (classes === undefined ? {} : classes); | ||||
|     self.filters = (filters === undefined ? [] : filters); | ||||
|     self.loaded = loaded; | ||||
|     self.order = order; | ||||
| 
 | ||||
|     self.limit = 20; | ||||
|     self.offset = 0; | ||||
|     self.filterOn = []; | ||||
|     self.facets = {}; | ||||
| 
 | ||||
|     // Index of column with author name; could be made more general by passing database column names and checking for position
 | ||||
|     self.columnIdAuthor = 2; | ||||
| 
 | ||||
|     // Future option: Create more general solution for filter presets
 | ||||
|     if (H5PIntegration.user && parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) === 1) { | ||||
|       self.updateTable([]); | ||||
|       self.filterByFacet(self.columnIdAuthor, H5PIntegration.user.id, H5PIntegration.user.name || ''); | ||||
|     } | ||||
|     else { | ||||
|       self.loadData(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Load data from source URL. | ||||
|    */ | ||||
|   H5PDataView.prototype.loadData = function () { | ||||
|     var self = this; | ||||
| 
 | ||||
|     // Throbb
 | ||||
|     self.setMessage(H5PUtils.throbber(self.l10n.loading)); | ||||
| 
 | ||||
|     // Create URL
 | ||||
|     var url = self.source; | ||||
|     url += (url.indexOf('?') === -1 ? '?' : '&') + 'offset=' + self.offset + '&limit=' + self.limit; | ||||
| 
 | ||||
|     // Add sorting
 | ||||
|     if (self.order !== undefined) { | ||||
|       url += '&sortBy=' + self.order.by + '&sortDir=' + self.order.dir; | ||||
|     } | ||||
| 
 | ||||
|     // Add filters
 | ||||
|     var filtering; | ||||
|     for (var i = 0; i < self.filterOn.length; i++) { | ||||
|       if (self.filterOn[i] === undefined) { | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       filtering = true; | ||||
|       url += '&filters[' + i + ']=' + encodeURIComponent(self.filterOn[i]); | ||||
|     } | ||||
| 
 | ||||
|     // Add facets
 | ||||
|     for (var col in self.facets) { | ||||
|       if (!self.facets.hasOwnProperty(col)) { | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       url += '&facets[' + col + ']=' + self.facets[col].id; | ||||
|     } | ||||
| 
 | ||||
|     // Fire ajax request
 | ||||
|     $.ajax({ | ||||
|       dataType: 'json', | ||||
|       cache: true, | ||||
|       url: url | ||||
|     }).fail(function () { | ||||
|       // Error handling
 | ||||
|       self.setMessage($('<p/>', {text: self.l10n.ajaxFailed})); | ||||
|     }).done(function (data) { | ||||
|       if (!data.rows.length) { | ||||
|         self.setMessage($('<p/>', {text: filtering ? self.l10n.noData : self.l10n.empty})); | ||||
|       } | ||||
|       else { | ||||
|         // Update table data
 | ||||
|         self.updateTable(data.rows); | ||||
|       } | ||||
| 
 | ||||
|       // Update pagination widget
 | ||||
|       self.updatePagination(data.num); | ||||
| 
 | ||||
|       if (self.loaded !== undefined) { | ||||
|         self.loaded(); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Display the given message to the user. | ||||
|    * | ||||
|    * @param {jQuery} $message wrapper with message | ||||
|    */ | ||||
|   H5PDataView.prototype.setMessage = function ($message) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (self.table === undefined) { | ||||
|       self.$container.html('').append($message); | ||||
|     } | ||||
|     else { | ||||
|       self.table.setBody($message); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Update table data. | ||||
|    * | ||||
|    * @param {Array} rows | ||||
|    */ | ||||
|   H5PDataView.prototype.updateTable = function (rows) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (self.table === undefined) { | ||||
|       // Clear out container
 | ||||
|       self.$container.html(''); | ||||
| 
 | ||||
|       // Add filters
 | ||||
|       self.addFilters(); | ||||
| 
 | ||||
|       // Add toggler for others' content
 | ||||
|       if (H5PIntegration.user && parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) > 0) { | ||||
|         // canToggleViewOthersH5PContents = 1 is setting for only showing current user's contents
 | ||||
|         self.addOthersContentToggler(parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) === 1); | ||||
|       } | ||||
| 
 | ||||
|       // Add facets
 | ||||
|       self.$facets = $('<div/>', { | ||||
|         'class': 'h5p-facet-wrapper', | ||||
|         appendTo: self.$container | ||||
|       }); | ||||
| 
 | ||||
|       // Create new table
 | ||||
|       self.table = new H5PUtils.Table(self.classes, self.headers); | ||||
|       self.table.setHeaders(self.headers, function (order) { | ||||
|         // Sorting column or direction has changed.
 | ||||
|         self.order = order; | ||||
|         self.loadData(); | ||||
|       }, self.order); | ||||
|       self.table.appendTo(self.$container); | ||||
|     } | ||||
| 
 | ||||
|     // Process cell data before updating table
 | ||||
|     for (var i = 0; i < self.headers.length; i++) { | ||||
|       if (self.headers[i].facet === true) { | ||||
|         // Process rows for col, expect object or array
 | ||||
|         for (var j = 0; j < rows.length; j++) { | ||||
|           rows[j][i] = self.createFacets(rows[j][i], i); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Add/update rows
 | ||||
|     var $tbody = self.table.setRows(rows); | ||||
| 
 | ||||
|     // Add event handlers for facets
 | ||||
|     $('.h5p-facet', $tbody).click(function () { | ||||
|       var $facet = $(this); | ||||
|       self.filterByFacet($facet.data('col'), $facet.data('id'), $facet.text()); | ||||
|     }).keypress(function (event) { | ||||
|       if (event.which === 32) { | ||||
|         var $facet = $(this); | ||||
|         self.filterByFacet($facet.data('col'), $facet.data('id'), $facet.text()); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Create button for adding facet to filter. | ||||
|    * | ||||
|    * @param (object|Array) input | ||||
|    * @param number col ID of column | ||||
|    */ | ||||
|   H5PDataView.prototype.createFacets = function (input, col) { | ||||
|     var facets = ''; | ||||
| 
 | ||||
|     if (input instanceof Array) { | ||||
|       // Facet can be filtered on multiple values at the same time
 | ||||
|       for (var i = 0; i < input.length; i++) { | ||||
|         if (facets !== '') { | ||||
|           facets += ', '; | ||||
|         } | ||||
|         facets += '<span class="h5p-facet" role="button" tabindex="0" data-id="' + input[i].id + '" data-col="' + col + '">' + input[i].title + '</span>'; | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
|       // Single value facet filtering
 | ||||
|       facets += '<span class="h5p-facet" role="button" tabindex="0" data-id="' + input.id + '" data-col="' + col + '">' + input.title + '</span>'; | ||||
|     } | ||||
| 
 | ||||
|     return facets === '' ? '—' : facets; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Adds a filter based on the given facet. | ||||
|    * | ||||
|    * @param number col ID of column we're filtering | ||||
|    * @param number id ID to filter on | ||||
|    * @param string text Human readable label for the filter | ||||
|    */ | ||||
|   H5PDataView.prototype.filterByFacet = function (col, id, text) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (self.facets[col] !== undefined) { | ||||
|       if (self.facets[col].id === id) { | ||||
|         return; // Don't use the same filter again
 | ||||
|       } | ||||
| 
 | ||||
|       // Remove current filter for this col
 | ||||
|       self.facets[col].$tag.remove(); | ||||
|     } | ||||
| 
 | ||||
|     // Add to UI
 | ||||
|     self.facets[col] = { | ||||
|       id: id, | ||||
|       '$tag': $('<span/>', { | ||||
|         'class': 'h5p-facet-tag', | ||||
|         text: text, | ||||
|         appendTo: self.$facets, | ||||
|       }) | ||||
|     }; | ||||
|     /** | ||||
|      * Callback for removing filter. | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     var remove = function () { | ||||
|       // Uncheck toggler for others' H5P contents
 | ||||
|       if ( self.$othersContentToggler && self.facets.hasOwnProperty( self.columnIdAuthor ) ) { | ||||
|         self.$othersContentToggler.prop('checked', false ); | ||||
|       } | ||||
| 
 | ||||
|       self.facets[col].$tag.remove(); | ||||
|       delete self.facets[col]; | ||||
|       self.loadData(); | ||||
|     }; | ||||
| 
 | ||||
|     // Remove button
 | ||||
|     $('<span/>', { | ||||
|       role: 'button', | ||||
|       tabindex: 0, | ||||
|       appendTo: self.facets[col].$tag, | ||||
|       text: self.l10n.remove, | ||||
|       title: self.l10n.remove, | ||||
|       on: { | ||||
|         click: remove, | ||||
|         keypress: function (event) { | ||||
|           if (event.which === 32) { | ||||
|             remove(); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     // Load data with new filter
 | ||||
|     self.loadData(); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Update pagination widget. | ||||
|    * | ||||
|    * @param {Number} num size of data collection | ||||
|    */ | ||||
|   H5PDataView.prototype.updatePagination = function (num) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     if (self.pagination === undefined) { | ||||
|       if (self.table === undefined) { | ||||
|         // No table, no pagination
 | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // Create new widget
 | ||||
|       var $pagerContainer = $('<div/>', {'class': 'h5p-pagination'}); | ||||
|       self.pagination = new H5PUtils.Pagination(num, self.limit, function (offset) { | ||||
|         // Handle page changes in pagination widget
 | ||||
|         self.offset = offset; | ||||
|         self.loadData(); | ||||
|       }, self.l10n); | ||||
| 
 | ||||
|       self.pagination.appendTo($pagerContainer); | ||||
|       self.table.setFoot($pagerContainer); | ||||
|     } | ||||
|     else { | ||||
|       // Update existing widget
 | ||||
|       self.pagination.update(num, self.limit); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Add filters. | ||||
|    */ | ||||
|   H5PDataView.prototype.addFilters = function () { | ||||
|     var self = this; | ||||
| 
 | ||||
|     for (var i = 0; i < self.filters.length; i++) { | ||||
|       if (self.filters[i] === true) { | ||||
|         // Add text input filter for col i
 | ||||
|         self.addTextFilter(i); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Add text filter for given col num. | ||||
|    * | ||||
|    * @param {Number} col | ||||
|    */ | ||||
|   H5PDataView.prototype.addTextFilter = function (col) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     /** | ||||
|      * Find input value and filter on it. | ||||
|      * @private | ||||
|      */ | ||||
|     var search = function () { | ||||
|       var filterOn = $input.val().replace(/^\s+|\s+$/g, ''); | ||||
|       if (filterOn === '') { | ||||
|         filterOn = undefined; | ||||
|       } | ||||
|       if (filterOn !== self.filterOn[col]) { | ||||
|         self.filterOn[col] = filterOn; | ||||
|         self.loadData(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Add text field for filtering
 | ||||
|     var typing; | ||||
|     var $input = $('<input/>', { | ||||
|       type: 'text', | ||||
|       placeholder: self.l10n.search, | ||||
|       on: { | ||||
|         'blur': function () { | ||||
|           clearTimeout(typing); | ||||
|           search(); | ||||
|         }, | ||||
|         'keyup': function (event) { | ||||
|           if (event.keyCode === 13) { | ||||
|             clearTimeout(typing); | ||||
|             search(); | ||||
|             return false; | ||||
|           } | ||||
|           else { | ||||
|             clearTimeout(typing); | ||||
|             typing = setTimeout(function () { | ||||
|               search(); | ||||
|             }, 500); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }).appendTo(self.$container); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Add toggle for others' H5P content. | ||||
|    * @param {boolean} [checked=false] Initial check setting. | ||||
|    */ | ||||
|   H5PDataView.prototype.addOthersContentToggler = function (checked) { | ||||
|     var self = this; | ||||
| 
 | ||||
|     checked = (typeof checked === 'undefined') ? false : checked; | ||||
| 
 | ||||
|     // Checkbox
 | ||||
|     this.$othersContentToggler = $('<input/>', { | ||||
|       type: 'checkbox', | ||||
|       'class': 'h5p-others-contents-toggler', | ||||
|       'id': 'h5p-others-contents-toggler', | ||||
|       'checked': checked, | ||||
|       'click': function () { | ||||
|         if ( this.checked ) { | ||||
|           // Add filter on current user
 | ||||
|           self.filterByFacet( self.columnIdAuthor, H5PIntegration.user.id, H5PIntegration.user.name ); | ||||
|         } | ||||
|         else { | ||||
|           // Remove facet indicator and reload full data view
 | ||||
|           if ( self.facets.hasOwnProperty( self.columnIdAuthor ) && self.facets[self.columnIdAuthor].$tag ) { | ||||
|             self.facets[self.columnIdAuthor].$tag.remove(); | ||||
|           } | ||||
|           delete self.facets[self.columnIdAuthor]; | ||||
|           self.loadData(); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     // Label
 | ||||
|     var $label = $('<label>', { | ||||
|       'class': 'h5p-others-contents-toggler-label', | ||||
|       'text': this.l10n.showOwnContentOnly, | ||||
|       'for': 'h5p-others-contents-toggler' | ||||
|     }).prepend(this.$othersContentToggler); | ||||
| 
 | ||||
|     $('<div>', { | ||||
|       'class': 'h5p-others-contents-toggler-wrapper' | ||||
|     }).append($label) | ||||
|       .appendTo(this.$container); | ||||
|   }; | ||||
| 
 | ||||
|   return H5PDataView; | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										54
									
								
								src/core/features/h5p/assets/js/h5p-display-options.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/core/features/h5p/assets/js/h5p-display-options.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| /** | ||||
|  * Utility that makes it possible to hide fields when a checkbox is unchecked | ||||
|  */ | ||||
| (function ($) { | ||||
|   function setupHiding() { | ||||
|     var $toggler = $(this); | ||||
| 
 | ||||
|     // Getting the field which should be hidden:
 | ||||
|     var $subject = $($toggler.data('h5p-visibility-subject-selector')); | ||||
| 
 | ||||
|     var toggle = function () { | ||||
|       $subject.toggle($toggler.is(':checked')); | ||||
|     }; | ||||
| 
 | ||||
|     $toggler.change(toggle); | ||||
|     toggle(); | ||||
|   } | ||||
| 
 | ||||
|   function setupRevealing() { | ||||
|     var $button = $(this); | ||||
| 
 | ||||
|     // Getting the field which should have the value:
 | ||||
|     var $input = $('#' + $button.data('control')); | ||||
| 
 | ||||
|     if (!$input.data('value')) { | ||||
|       $button.remove(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Setup button action
 | ||||
|     var revealed = false; | ||||
|     var text = $button.html(); | ||||
|     $button.click(function () { | ||||
|       if (revealed) { | ||||
|         $input.val(''); | ||||
|         $button.html(text); | ||||
|         revealed = false; | ||||
|       } | ||||
|       else { | ||||
|         $input.val($input.data('value')); | ||||
|         $button.html($button.data('hide')); | ||||
|         revealed = true; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   $(document).ready(function () { | ||||
|     // Get the checkboxes making other fields being hidden:
 | ||||
|     $('.h5p-visibility-toggler').each(setupHiding); | ||||
| 
 | ||||
|     // Get the buttons making other fields have hidden values:
 | ||||
|     $('.h5p-reveal-value').each(setupRevealing); | ||||
|   }); | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										75
									
								
								src/core/features/h5p/assets/js/h5p-embed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/core/features/h5p/assets/js/h5p-embed.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| /*jshint multistr: true */ | ||||
| 
 | ||||
| /** | ||||
|  * Converts old script tag embed to iframe | ||||
|  */ | ||||
| var H5POldEmbed = H5POldEmbed || (function () { | ||||
|   var head = document.getElementsByTagName('head')[0]; | ||||
|   var resizer = false; | ||||
| 
 | ||||
|   /** | ||||
|    * Loads the resizing script | ||||
|    */ | ||||
|   var loadResizer = function (url) { | ||||
|     var data, callback = 'H5POldEmbed'; | ||||
|     resizer = true; | ||||
| 
 | ||||
|     // Callback for when content data is loaded.
 | ||||
|     window[callback] = function (content) { | ||||
|       // Add resizing script to head
 | ||||
|       var resizer = document.createElement('script'); | ||||
|       resizer.src = content; | ||||
|       head.appendChild(resizer); | ||||
| 
 | ||||
|       // Clean up
 | ||||
|       head.removeChild(data); | ||||
|       delete window[callback]; | ||||
|     }; | ||||
| 
 | ||||
|     // Create data script
 | ||||
|     data = document.createElement('script'); | ||||
|     data.src = url + (url.indexOf('?') === -1 ? '?' : '&') + 'callback=' + callback; | ||||
|     head.appendChild(data); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Replaced script tag with iframe | ||||
|    */ | ||||
|   var addIframe = function (script) { | ||||
|     // Add iframe
 | ||||
|     var iframe = document.createElement('iframe'); | ||||
|     iframe.src = script.getAttribute('data-h5p'); | ||||
|     iframe.frameBorder = false; | ||||
|     iframe.allowFullscreen = true; | ||||
|     var parent = script.parentNode; | ||||
|     parent.insertBefore(iframe, script); | ||||
|     parent.removeChild(script); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Go throught all script tags with the data-h5p attribute and load content. | ||||
|    */ | ||||
|   function H5POldEmbed() { | ||||
|     var scripts = document.getElementsByTagName('script'); | ||||
|     var h5ps = []; // Use seperate array since scripts grow in size.
 | ||||
|     for (var i = 0; i < scripts.length; i++) { | ||||
|       var script = scripts[i]; | ||||
|       if (script.src.indexOf('/h5p-resizer.js') !== -1) { | ||||
|         resizer = true; | ||||
|       } | ||||
|       else if (script.hasAttribute('data-h5p')) { | ||||
|         h5ps.push(script); | ||||
|       } | ||||
|     } | ||||
|     for (i = 0; i < h5ps.length; i++) { | ||||
|       if (!resizer) { | ||||
|         loadResizer(h5ps[i].getAttribute('data-h5p')); | ||||
|       } | ||||
|       addIframe(h5ps[i]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return H5POldEmbed; | ||||
| })(); | ||||
| 
 | ||||
| new H5POldEmbed(); | ||||
							
								
								
									
										258
									
								
								src/core/features/h5p/assets/js/h5p-event-dispatcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								src/core/features/h5p/assets/js/h5p-event-dispatcher.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,258 @@ | ||||
| var H5P = window.H5P = window.H5P || {}; | ||||
| 
 | ||||
| /** | ||||
|  * The Event class for the EventDispatcher. | ||||
|  * | ||||
|  * @class | ||||
|  * @param {string} type | ||||
|  * @param {*} data | ||||
|  * @param {Object} [extras] | ||||
|  * @param {boolean} [extras.bubbles] | ||||
|  * @param {boolean} [extras.external] | ||||
|  */ | ||||
| H5P.Event = function (type, data, extras) { | ||||
|   this.type = type; | ||||
|   this.data = data; | ||||
|   var bubbles = false; | ||||
| 
 | ||||
|   // Is this an external event?
 | ||||
|   var external = false; | ||||
| 
 | ||||
|   // Is this event scheduled to be sent externally?
 | ||||
|   var scheduledForExternal = false; | ||||
| 
 | ||||
|   if (extras === undefined) { | ||||
|     extras = {}; | ||||
|   } | ||||
|   if (extras.bubbles === true) { | ||||
|     bubbles = true; | ||||
|   } | ||||
|   if (extras.external === true) { | ||||
|     external = true; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Prevent this event from bubbling up to parent | ||||
|    */ | ||||
|   this.preventBubbling = function () { | ||||
|     bubbles = false; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Get bubbling status | ||||
|    * | ||||
|    * @returns {boolean} | ||||
|    *   true if bubbling false otherwise | ||||
|    */ | ||||
|   this.getBubbles = function () { | ||||
|     return bubbles; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Try to schedule an event for externalDispatcher | ||||
|    * | ||||
|    * @returns {boolean} | ||||
|    *   true if external and not already scheduled, otherwise false | ||||
|    */ | ||||
|   this.scheduleForExternal = function () { | ||||
|     if (external && !scheduledForExternal) { | ||||
|       scheduledForExternal = true; | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Callback type for event listeners. | ||||
|  * | ||||
|  * @callback H5P.EventCallback | ||||
|  * @param {H5P.Event} event | ||||
|  */ | ||||
| 
 | ||||
| H5P.EventDispatcher = (function () { | ||||
| 
 | ||||
|   /** | ||||
|    * The base of the event system. | ||||
|    * Inherit this class if you want your H5P to dispatch events. | ||||
|    * | ||||
|    * @class | ||||
|    * @memberof H5P | ||||
|    */ | ||||
|   function EventDispatcher() { | ||||
|     var self = this; | ||||
| 
 | ||||
|     /** | ||||
|      * Keep track of listeners for each event. | ||||
|      * | ||||
|      * @private | ||||
|      * @type {Object} | ||||
|      */ | ||||
|     var triggers = {}; | ||||
| 
 | ||||
|     /** | ||||
|      * Add new event listener. | ||||
|      * | ||||
|      * @throws {TypeError} | ||||
|      *   listener must be a function | ||||
|      * @param {string} type | ||||
|      *   Event type | ||||
|      * @param {H5P.EventCallback} listener | ||||
|      *   Event listener | ||||
|      * @param {Object} [thisArg] | ||||
|      *   Optionally specify the this value when calling listener. | ||||
|      */ | ||||
|     this.on = function (type, listener, thisArg) { | ||||
|       if (typeof listener !== 'function') { | ||||
|         throw TypeError('listener must be a function'); | ||||
|       } | ||||
| 
 | ||||
|       // Trigger event before adding to avoid recursion
 | ||||
|       self.trigger('newListener', {'type': type, 'listener': listener}); | ||||
| 
 | ||||
|       var trigger = {'listener': listener, 'thisArg': thisArg}; | ||||
|       if (!triggers[type]) { | ||||
|         // First
 | ||||
|         triggers[type] = [trigger]; | ||||
|       } | ||||
|       else { | ||||
|         // Append
 | ||||
|         triggers[type].push(trigger); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Add new event listener that will be fired only once. | ||||
|      * | ||||
|      * @throws {TypeError} | ||||
|      *   listener must be a function | ||||
|      * @param {string} type | ||||
|      *   Event type | ||||
|      * @param {H5P.EventCallback} listener | ||||
|      *   Event listener | ||||
|      * @param {Object} thisArg | ||||
|      *   Optionally specify the this value when calling listener. | ||||
|      */ | ||||
|     this.once = function (type, listener, thisArg) { | ||||
|       if (!(listener instanceof Function)) { | ||||
|         throw TypeError('listener must be a function'); | ||||
|       } | ||||
| 
 | ||||
|       var once = function (event) { | ||||
|         self.off(event.type, once); | ||||
|         listener.call(this, event); | ||||
|       }; | ||||
| 
 | ||||
|       self.on(type, once, thisArg); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Remove event listener. | ||||
|      * If no listener is specified, all listeners will be removed. | ||||
|      * | ||||
|      * @throws {TypeError} | ||||
|      *   listener must be a function | ||||
|      * @param {string} type | ||||
|      *   Event type | ||||
|      * @param {H5P.EventCallback} listener | ||||
|      *   Event listener | ||||
|      */ | ||||
|     this.off = function (type, listener) { | ||||
|       if (listener !== undefined && !(listener instanceof Function)) { | ||||
|         throw TypeError('listener must be a function'); | ||||
|       } | ||||
| 
 | ||||
|       if (triggers[type] === undefined) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (listener === undefined) { | ||||
|         // Remove all listeners
 | ||||
|         delete triggers[type]; | ||||
|         self.trigger('removeListener', type); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // Find specific listener
 | ||||
|       for (var i = 0; i < triggers[type].length; i++) { | ||||
|         if (triggers[type][i].listener === listener) { | ||||
|           triggers[type].splice(i, 1); | ||||
|           self.trigger('removeListener', type, {'listener': listener}); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Clean up empty arrays
 | ||||
|       if (!triggers[type].length) { | ||||
|         delete triggers[type]; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Try to call all event listeners for the given event type. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {string} Event type | ||||
|      */ | ||||
|     var call = function (type, event) { | ||||
|       if (triggers[type] === undefined) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // Clone array (prevents triggers from being modified during the event)
 | ||||
|       var handlers = triggers[type].slice(); | ||||
| 
 | ||||
|       // Call all listeners
 | ||||
|       for (var i = 0; i < handlers.length; i++) { | ||||
|         var trigger = handlers[i]; | ||||
|         var thisArg = (trigger.thisArg ? trigger.thisArg : this); | ||||
|         trigger.listener.call(thisArg, event); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Dispatch event. | ||||
|      * | ||||
|      * @param {string|H5P.Event} event | ||||
|      *   Event object or event type as string | ||||
|      * @param {*} [eventData] | ||||
|      *   Custom event data(used when event type as string is used as first | ||||
|      *   argument). | ||||
|      * @param {Object} [extras] | ||||
|      * @param {boolean} [extras.bubbles] | ||||
|      * @param {boolean} [extras.external] | ||||
|      */ | ||||
|     this.trigger = function (event, eventData, extras) { | ||||
|       if (event === undefined) { | ||||
|         return; | ||||
|       } | ||||
|       if (event instanceof String || typeof event === 'string') { | ||||
|         event = new H5P.Event(event, eventData, extras); | ||||
|       } | ||||
|       else if (eventData !== undefined) { | ||||
|         event.data = eventData; | ||||
|       } | ||||
| 
 | ||||
|       // Check to see if this event should go externally after all triggering and bubbling is done
 | ||||
|       var scheduledForExternal = event.scheduleForExternal(); | ||||
| 
 | ||||
|       // Call all listeners
 | ||||
|       call.call(this, event.type, event); | ||||
| 
 | ||||
|       // Call all * listeners
 | ||||
|       call.call(this, '*', event); | ||||
| 
 | ||||
|       // Bubble
 | ||||
|       if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && | ||||
|           (self.parent.trigger instanceof Function || typeof self.parent.trigger === 'function')) { | ||||
|         self.parent.trigger(event); | ||||
|       } | ||||
| 
 | ||||
|       if (scheduledForExternal) { | ||||
|         H5P.externalDispatcher.trigger.call(this, event); | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return EventDispatcher; | ||||
| })(); | ||||
							
								
								
									
										297
									
								
								src/core/features/h5p/assets/js/h5p-library-details.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/core/features/h5p/assets/js/h5p-library-details.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,297 @@ | ||||
| /* global H5PAdminIntegration H5PUtils */ | ||||
| var H5PLibraryDetails = H5PLibraryDetails || {}; | ||||
| 
 | ||||
| (function ($) { | ||||
| 
 | ||||
|   H5PLibraryDetails.PAGER_SIZE = 20; | ||||
|   /** | ||||
|    * Initializing | ||||
|    */ | ||||
|   H5PLibraryDetails.init = function () { | ||||
|     H5PLibraryDetails.$adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector); | ||||
|     H5PLibraryDetails.library = H5PAdminIntegration.libraryInfo; | ||||
| 
 | ||||
|     // currentContent holds the current list if data (relevant for filtering)
 | ||||
|     H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; | ||||
| 
 | ||||
|     // The current page index (for pager)
 | ||||
|     H5PLibraryDetails.currentPage = 0; | ||||
| 
 | ||||
|     // The current filter
 | ||||
|     H5PLibraryDetails.currentFilter = ''; | ||||
| 
 | ||||
|     // We cache the filtered results, so we don't have to do unneccessary searches
 | ||||
|     H5PLibraryDetails.filterCache = []; | ||||
| 
 | ||||
|     // Append library info
 | ||||
|     H5PLibraryDetails.$adminContainer.append(H5PLibraryDetails.createLibraryInfo()); | ||||
| 
 | ||||
|     // Append node list
 | ||||
|     H5PLibraryDetails.$adminContainer.append(H5PLibraryDetails.createContentElement()); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Create the library details view | ||||
|    */ | ||||
|   H5PLibraryDetails.createLibraryInfo = function () { | ||||
|     var $libraryInfo = $('<div class="h5p-library-info"></div>'); | ||||
| 
 | ||||
|     $.each(H5PLibraryDetails.library.info, function (title, value) { | ||||
|       $libraryInfo.append(H5PUtils.createLabeledField(title, value)); | ||||
|     }); | ||||
| 
 | ||||
|     return $libraryInfo; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Create the content list with searching and paging | ||||
|    */ | ||||
|   H5PLibraryDetails.createContentElement = function () { | ||||
|     if (H5PLibraryDetails.library.notCached !== undefined) { | ||||
|       return H5PUtils.getRebuildCache(H5PLibraryDetails.library.notCached); | ||||
|     } | ||||
| 
 | ||||
|     if (H5PLibraryDetails.currentContent === undefined) { | ||||
|       H5PLibraryDetails.$content = $('<div class="h5p-content empty">' + H5PLibraryDetails.library.translations.noContent + '</div>'); | ||||
|     } | ||||
|     else { | ||||
|       H5PLibraryDetails.$content = $('<div class="h5p-content"><h3>' + H5PLibraryDetails.library.translations.contentHeader + '</h3></div>'); | ||||
|       H5PLibraryDetails.createSearchElement(); | ||||
|       H5PLibraryDetails.createPageSizeSelector(); | ||||
|       H5PLibraryDetails.createContentTable(); | ||||
|       H5PLibraryDetails.createPagerElement(); | ||||
|       return H5PLibraryDetails.$content; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the content list | ||||
|    */ | ||||
|   H5PLibraryDetails.createContentTable = function () { | ||||
|     // Remove it if it exists:
 | ||||
|     if (H5PLibraryDetails.$contentTable) { | ||||
|       H5PLibraryDetails.$contentTable.remove(); | ||||
|     } | ||||
| 
 | ||||
|     H5PLibraryDetails.$contentTable = H5PUtils.createTable(); | ||||
| 
 | ||||
|     var i = (H5PLibraryDetails.currentPage*H5PLibraryDetails.PAGER_SIZE); | ||||
|     var lastIndex = (i+H5PLibraryDetails.PAGER_SIZE); | ||||
| 
 | ||||
|     if (lastIndex > H5PLibraryDetails.currentContent.length) { | ||||
|       lastIndex = H5PLibraryDetails.currentContent.length; | ||||
|     } | ||||
|     for (; i<lastIndex; i++) { | ||||
|       var content = H5PLibraryDetails.currentContent[i]; | ||||
|       H5PLibraryDetails.$contentTable.append(H5PUtils.createTableRow(['<a href="' + content.url + '">' + content.title + '</a>'])); | ||||
|     } | ||||
| 
 | ||||
|     // Appends it to the browser DOM
 | ||||
|     H5PLibraryDetails.$contentTable.insertAfter(H5PLibraryDetails.$search); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the pager element on the bottom of the list | ||||
|    */ | ||||
|   H5PLibraryDetails.createPagerElement = function () { | ||||
|     H5PLibraryDetails.$previous = $('<button type="button" class="previous h5p-admin"><</button>'); | ||||
|     H5PLibraryDetails.$next = $('<button type="button" class="next h5p-admin">></button>'); | ||||
| 
 | ||||
|     H5PLibraryDetails.$previous.on('click', function () { | ||||
|       if (H5PLibraryDetails.$previous.hasClass('disabled')) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       H5PLibraryDetails.currentPage--; | ||||
|       H5PLibraryDetails.updatePager(); | ||||
|       H5PLibraryDetails.createContentTable(); | ||||
|     }); | ||||
| 
 | ||||
|     H5PLibraryDetails.$next.on('click', function () { | ||||
|       if (H5PLibraryDetails.$next.hasClass('disabled')) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       H5PLibraryDetails.currentPage++; | ||||
|       H5PLibraryDetails.updatePager(); | ||||
|       H5PLibraryDetails.createContentTable(); | ||||
|     }); | ||||
| 
 | ||||
|     // This is the Page x of y widget:
 | ||||
|     H5PLibraryDetails.$pagerInfo = $('<span class="pager-info"></span>'); | ||||
| 
 | ||||
|     H5PLibraryDetails.$pager = $('<div class="h5p-content-pager"></div>').append(H5PLibraryDetails.$previous, H5PLibraryDetails.$pagerInfo, H5PLibraryDetails.$next); | ||||
|     H5PLibraryDetails.$content.append(H5PLibraryDetails.$pager); | ||||
| 
 | ||||
|     H5PLibraryDetails.$pagerInfo.on('click', function () { | ||||
|       var width = H5PLibraryDetails.$pagerInfo.innerWidth(); | ||||
|       H5PLibraryDetails.$pagerInfo.hide(); | ||||
| 
 | ||||
|       // User has updated the pageNumber
 | ||||
|       var pageNumerUpdated = function () { | ||||
|         var newPageNum = $gotoInput.val()-1; | ||||
|         var intRegex = /^\d+$/; | ||||
| 
 | ||||
|         $goto.remove(); | ||||
|         H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); | ||||
| 
 | ||||
|         // Check if input value is valid, and that it has actually changed
 | ||||
|         if (!(intRegex.test(newPageNum) && newPageNum >= 0 && newPageNum < H5PLibraryDetails.getNumPages() && newPageNum != H5PLibraryDetails.currentPage)) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         H5PLibraryDetails.currentPage = newPageNum; | ||||
|         H5PLibraryDetails.updatePager(); | ||||
|         H5PLibraryDetails.createContentTable(); | ||||
|       }; | ||||
| 
 | ||||
|       // We create an input box where the user may type in the page number
 | ||||
|       // he wants to be displayed.
 | ||||
|       // Reson for doing this is when user has ten-thousands of elements in list,
 | ||||
|       // this is the easiest way of getting to a specified page
 | ||||
|       var $gotoInput = $('<input/>', { | ||||
|         type: 'number', | ||||
|         min : 1, | ||||
|         max: H5PLibraryDetails.getNumPages(), | ||||
|         on: { | ||||
|           // Listen to blur, and the enter-key:
 | ||||
|           'blur': pageNumerUpdated, | ||||
|           'keyup': function (event) { | ||||
|             if (event.keyCode === 13) { | ||||
|               pageNumerUpdated(); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       }).css({width: width}); | ||||
|       var $goto = $('<span/>', { | ||||
|         'class': 'h5p-pager-goto' | ||||
|       }).css({width: width}).append($gotoInput).insertAfter(H5PLibraryDetails.$pagerInfo); | ||||
| 
 | ||||
|       $gotoInput.focus(); | ||||
|     }); | ||||
| 
 | ||||
|     H5PLibraryDetails.updatePager(); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Calculates number of pages | ||||
|    */ | ||||
|   H5PLibraryDetails.getNumPages = function () { | ||||
|     return Math.ceil(H5PLibraryDetails.currentContent.length / H5PLibraryDetails.PAGER_SIZE); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Update the pager text, and enables/disables the next and previous buttons as needed | ||||
|    */ | ||||
|   H5PLibraryDetails.updatePager = function () { | ||||
|     H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); | ||||
| 
 | ||||
|     if (H5PLibraryDetails.getNumPages() > 0) { | ||||
|       var message = H5PUtils.translateReplace(H5PLibraryDetails.library.translations.pageXOfY, { | ||||
|         '$x': (H5PLibraryDetails.currentPage+1), | ||||
|         '$y': H5PLibraryDetails.getNumPages() | ||||
|       }); | ||||
|       H5PLibraryDetails.$pagerInfo.html(message); | ||||
|     } | ||||
|     else { | ||||
|       H5PLibraryDetails.$pagerInfo.html(''); | ||||
|     } | ||||
| 
 | ||||
|     H5PLibraryDetails.$previous.toggleClass('disabled', H5PLibraryDetails.currentPage <= 0); | ||||
|     H5PLibraryDetails.$next.toggleClass('disabled', H5PLibraryDetails.currentContent.length < (H5PLibraryDetails.currentPage+1)*H5PLibraryDetails.PAGER_SIZE); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the search element | ||||
|    */ | ||||
|   H5PLibraryDetails.createSearchElement = function () { | ||||
| 
 | ||||
|     H5PLibraryDetails.$search = $('<div class="h5p-content-search"><input placeholder="' + H5PLibraryDetails.library.translations.filterPlaceholder + '" type="search"></div>'); | ||||
| 
 | ||||
|     var performSeach = function () { | ||||
|       var searchString = $('.h5p-content-search > input').val(); | ||||
| 
 | ||||
|       // If search string same as previous, just do nothing
 | ||||
|       if (H5PLibraryDetails.currentFilter === searchString) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (searchString.trim().length === 0) { | ||||
|         // If empty search, use the complete list
 | ||||
|         H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; | ||||
|       } | ||||
|       else if (H5PLibraryDetails.filterCache[searchString]) { | ||||
|         // If search is cached, no need to filter
 | ||||
|         H5PLibraryDetails.currentContent = H5PLibraryDetails.filterCache[searchString]; | ||||
|       } | ||||
|       else { | ||||
|         var listToFilter = H5PLibraryDetails.library.content; | ||||
| 
 | ||||
|         // Check if we can filter the already filtered results (for performance)
 | ||||
|         if (searchString.length > 1 && H5PLibraryDetails.currentFilter === searchString.substr(0, H5PLibraryDetails.currentFilter.length)) { | ||||
|           listToFilter = H5PLibraryDetails.currentContent; | ||||
|         } | ||||
|         H5PLibraryDetails.currentContent = $.grep(listToFilter, function (content) { | ||||
|           return content.title && content.title.match(new RegExp(searchString, 'i')); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       H5PLibraryDetails.currentFilter = searchString; | ||||
|       // Cache the current result
 | ||||
|       H5PLibraryDetails.filterCache[searchString] = H5PLibraryDetails.currentContent; | ||||
|       H5PLibraryDetails.currentPage = 0; | ||||
|       H5PLibraryDetails.createContentTable(); | ||||
| 
 | ||||
|       // Display search results:
 | ||||
|       if (H5PLibraryDetails.$searchResults) { | ||||
|         H5PLibraryDetails.$searchResults.remove(); | ||||
|       } | ||||
|       if (searchString.trim().length > 0) { | ||||
|         H5PLibraryDetails.$searchResults = $('<span class="h5p-admin-search-results">' + H5PLibraryDetails.currentContent.length + ' hits on ' + H5PLibraryDetails.currentFilter + '</span>'); | ||||
|         H5PLibraryDetails.$search.append(H5PLibraryDetails.$searchResults); | ||||
|       } | ||||
|       H5PLibraryDetails.updatePager(); | ||||
|     }; | ||||
| 
 | ||||
|     var inputTimer; | ||||
|     $('input', H5PLibraryDetails.$search).on('change keypress paste input', function () { | ||||
|       // Here we start the filtering
 | ||||
|       // We wait at least 500 ms after last input to perform search
 | ||||
|       if (inputTimer) { | ||||
|         clearTimeout(inputTimer); | ||||
|       } | ||||
| 
 | ||||
|       inputTimer = setTimeout( function () { | ||||
|         performSeach(); | ||||
|       }, 500); | ||||
|     }); | ||||
| 
 | ||||
|     H5PLibraryDetails.$content.append(H5PLibraryDetails.$search); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the page size selector | ||||
|    */ | ||||
|   H5PLibraryDetails.createPageSizeSelector = function () { | ||||
|     H5PLibraryDetails.$search.append('<div class="h5p-admin-pager-size-selector">' + H5PLibraryDetails.library.translations.pageSizeSelectorLabel + ':<span data-page-size="10">10</span><span class="selected" data-page-size="20">20</span><span data-page-size="50">50</span><span data-page-size="100">100</span><span data-page-size="200">200</span></div>'); | ||||
| 
 | ||||
|     // Listen to clicks on the page size selector:
 | ||||
|     $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).on('click', function () { | ||||
|       H5PLibraryDetails.PAGER_SIZE = $(this).data('page-size'); | ||||
|       $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).removeClass('selected'); | ||||
|       $(this).addClass('selected'); | ||||
|       H5PLibraryDetails.currentPage = 0; | ||||
|       H5PLibraryDetails.createContentTable(); | ||||
|       H5PLibraryDetails.updatePager(); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   // Initialize me:
 | ||||
|   $(document).ready(function () { | ||||
|     if (!H5PLibraryDetails.initialized) { | ||||
|       H5PLibraryDetails.initialized = true; | ||||
|       H5PLibraryDetails.init(); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										140
									
								
								src/core/features/h5p/assets/js/h5p-library-list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/core/features/h5p/assets/js/h5p-library-list.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,140 @@ | ||||
| /* global H5PAdminIntegration H5PUtils */ | ||||
| var H5PLibraryList = H5PLibraryList || {}; | ||||
| 
 | ||||
| (function ($) { | ||||
| 
 | ||||
|   /** | ||||
|    * Initializing | ||||
|    */ | ||||
|   H5PLibraryList.init = function () { | ||||
|     var $adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector).html(''); | ||||
| 
 | ||||
|     var libraryList = H5PAdminIntegration.libraryList; | ||||
|     if (libraryList.notCached) { | ||||
|       $adminContainer.append(H5PUtils.getRebuildCache(libraryList.notCached)); | ||||
|     } | ||||
| 
 | ||||
|     // Create library list
 | ||||
|     $adminContainer.append(H5PLibraryList.createLibraryList(H5PAdminIntegration.libraryList)); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Create the library list | ||||
|    * | ||||
|    * @param {object} libraries List of libraries and headers | ||||
|    */ | ||||
|   H5PLibraryList.createLibraryList = function (libraries) { | ||||
|     var t = H5PAdminIntegration.l10n; | ||||
|     if (libraries.listData === undefined || libraries.listData.length === 0) { | ||||
|       return $('<div>' + t.NA + '</div>'); | ||||
|     } | ||||
| 
 | ||||
|     // Create table
 | ||||
|     var $table = H5PUtils.createTable(libraries.listHeaders); | ||||
|     $table.addClass('libraries'); | ||||
| 
 | ||||
|     // Add libraries
 | ||||
|     $.each (libraries.listData, function (index, library) { | ||||
|       var $libraryRow = H5PUtils.createTableRow([ | ||||
|         library.title, | ||||
|         '<input class="h5p-admin-restricted" type="checkbox"/>', | ||||
|         { | ||||
|           text: library.numContent, | ||||
|           class: 'h5p-admin-center' | ||||
|         }, | ||||
|         { | ||||
|           text: library.numContentDependencies, | ||||
|           class: 'h5p-admin-center' | ||||
|         }, | ||||
|         { | ||||
|           text: library.numLibraryDependencies, | ||||
|           class: 'h5p-admin-center' | ||||
|         }, | ||||
|         '<div class="h5p-admin-buttons-wrapper">' + | ||||
|             '<button class="h5p-admin-upgrade-library"></button>' + | ||||
|             (library.detailsUrl ? '<button class="h5p-admin-view-library" title="' + t.viewLibrary + '"></button>' : '') + | ||||
|             (library.deleteUrl ? '<button class="h5p-admin-delete-library"></button>' : '') + | ||||
|             '</div>' | ||||
|       ]); | ||||
| 
 | ||||
|       H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted); | ||||
| 
 | ||||
|       var hasContent = !(library.numContent === '' || library.numContent === 0); | ||||
|       if (library.upgradeUrl === null) { | ||||
|         $('.h5p-admin-upgrade-library', $libraryRow).remove(); | ||||
|       } | ||||
|       else if (library.upgradeUrl === false || !hasContent) { | ||||
|         $('.h5p-admin-upgrade-library', $libraryRow).attr('disabled', true); | ||||
|       } | ||||
|       else { | ||||
|         $('.h5p-admin-upgrade-library', $libraryRow).attr('title', t.upgradeLibrary).click(function () { | ||||
|           window.location.href = library.upgradeUrl; | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       // Open details view when clicked
 | ||||
|       $('.h5p-admin-view-library', $libraryRow).on('click', function () { | ||||
|         window.location.href = library.detailsUrl; | ||||
|       }); | ||||
| 
 | ||||
|       var $deleteButton = $('.h5p-admin-delete-library', $libraryRow); | ||||
|       if (libraries.notCached !== undefined || | ||||
|           hasContent || | ||||
|           (library.numContentDependencies !== '' && | ||||
|            library.numContentDependencies !== 0) || | ||||
|           (library.numLibraryDependencies !== '' && | ||||
|            library.numLibraryDependencies !== 0)) { | ||||
|         // Disabled delete if content.
 | ||||
|         $deleteButton.attr('disabled', true); | ||||
|       } | ||||
|       else { | ||||
|         // Go to delete page om click.
 | ||||
|         $deleteButton.attr('title', t.deleteLibrary).on('click', function () { | ||||
|           window.location.href = library.deleteUrl; | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       $table.append($libraryRow); | ||||
|     }); | ||||
| 
 | ||||
|     return $table; | ||||
|   }; | ||||
| 
 | ||||
|   H5PLibraryList.addRestricted = function ($checkbox, url, selected) { | ||||
|     if (selected === null) { | ||||
|       $checkbox.remove(); | ||||
|     } | ||||
|     else { | ||||
|       $checkbox.change(function () { | ||||
|         $checkbox.attr('disabled', true); | ||||
| 
 | ||||
|         $.ajax({ | ||||
|           dataType: 'json', | ||||
|           url: url, | ||||
|           cache: false | ||||
|         }).fail(function () { | ||||
|           $checkbox.attr('disabled', false); | ||||
| 
 | ||||
|           // Reset
 | ||||
|           $checkbox.attr('checked', !$checkbox.is(':checked')); | ||||
|         }).done(function (result) { | ||||
|           url = result.url; | ||||
|           $checkbox.attr('disabled', false); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       if (selected) { | ||||
|         $checkbox.attr('checked', true); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Initialize me:
 | ||||
|   $(document).ready(function () { | ||||
|     if (!H5PLibraryList.initialized) { | ||||
|       H5PLibraryList.initialized = true; | ||||
|       H5PLibraryList.init(); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										131
									
								
								src/core/features/h5p/assets/js/h5p-resizer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/core/features/h5p/assets/js/h5p-resizer.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| // H5P iframe Resizer
 | ||||
| (function () { | ||||
|   if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { | ||||
|     return; // Not supported
 | ||||
|   } | ||||
|   window.h5pResizerInitialized = true; | ||||
| 
 | ||||
|   // Map actions to handlers
 | ||||
|   var actionHandlers = {}; | ||||
| 
 | ||||
|   /** | ||||
|    * Prepare iframe resize. | ||||
|    * | ||||
|    * @private | ||||
|    * @param {Object} iframe Element | ||||
|    * @param {Object} data Payload | ||||
|    * @param {Function} respond Send a response to the iframe | ||||
|    */ | ||||
|   actionHandlers.hello = function (iframe, data, respond) { | ||||
|     // Make iframe responsive
 | ||||
|     iframe.style.width = '100%'; | ||||
| 
 | ||||
|     // Bugfix for Chrome: Force update of iframe width. If this is not done the
 | ||||
|     // document size may not be updated before the content resizes.
 | ||||
|     iframe.getBoundingClientRect(); | ||||
| 
 | ||||
|     // Tell iframe that it needs to resize when our window resizes
 | ||||
|     var resize = function () { | ||||
|       if (iframe.contentWindow) { | ||||
|         // Limit resize calls to avoid flickering
 | ||||
|         respond('resize'); | ||||
|       } | ||||
|       else { | ||||
|         // Frame is gone, unregister.
 | ||||
|         window.removeEventListener('resize', resize); | ||||
|       } | ||||
|     }; | ||||
|     window.addEventListener('resize', resize, false); | ||||
| 
 | ||||
|     // Respond to let the iframe know we can resize it
 | ||||
|     respond('hello'); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Prepare iframe resize. | ||||
|    * | ||||
|    * @private | ||||
|    * @param {Object} iframe Element | ||||
|    * @param {Object} data Payload | ||||
|    * @param {Function} respond Send a response to the iframe | ||||
|    */ | ||||
|   actionHandlers.prepareResize = function (iframe, data, respond) { | ||||
|     // Do not resize unless page and scrolling differs
 | ||||
|     if (iframe.clientHeight !== data.scrollHeight || | ||||
|         data.scrollHeight !== data.clientHeight) { | ||||
| 
 | ||||
|       // Reset iframe height, in case content has shrinked.
 | ||||
|       iframe.style.height = data.clientHeight + 'px'; | ||||
|       respond('resizePrepared'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Resize parent and iframe to desired height. | ||||
|    * | ||||
|    * @private | ||||
|    * @param {Object} iframe Element | ||||
|    * @param {Object} data Payload | ||||
|    * @param {Function} respond Send a response to the iframe | ||||
|    */ | ||||
|   actionHandlers.resize = function (iframe, data) { | ||||
|     // Resize iframe so all content is visible. Use scrollHeight to make sure we get everything
 | ||||
|     iframe.style.height = data.scrollHeight + 'px'; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Keyup event handler. Exits full screen on escape. | ||||
|    * | ||||
|    * @param {Event} event | ||||
|    */ | ||||
|   var escape = function (event) { | ||||
|     if (event.keyCode === 27) { | ||||
|       exitFullScreen(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Listen for messages from iframes
 | ||||
|   window.addEventListener('message', function receiveMessage(event) { | ||||
|     if (event.data.context !== 'h5p') { | ||||
|       return; // Only handle h5p requests.
 | ||||
|     } | ||||
| 
 | ||||
|     // Find out who sent the message
 | ||||
|     var iframe, iframes = document.getElementsByTagName('iframe'); | ||||
|     for (var i = 0; i < iframes.length; i++) { | ||||
|       if (iframes[i].contentWindow === event.source) { | ||||
|         iframe = iframes[i]; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (!iframe) { | ||||
|       return; // Cannot find sender
 | ||||
|     } | ||||
| 
 | ||||
|     // Find action handler handler
 | ||||
|     if (actionHandlers[event.data.action]) { | ||||
|       actionHandlers[event.data.action](iframe, event.data, function respond(action, data) { | ||||
|         if (data === undefined) { | ||||
|           data = {}; | ||||
|         } | ||||
|         data.action = action; | ||||
|         data.context = 'h5p'; | ||||
|         event.source.postMessage(data, event.origin); | ||||
|       }); | ||||
|     } | ||||
|   }, false); | ||||
| 
 | ||||
|   // Let h5p iframes know we're ready!
 | ||||
|   var iframes = document.getElementsByTagName('iframe'); | ||||
|   var ready = { | ||||
|     context: 'h5p', | ||||
|     action: 'ready' | ||||
|   }; | ||||
|   for (var i = 0; i < iframes.length; i++) { | ||||
|     if (iframes[i].src.indexOf('h5p') !== -1) { | ||||
|       iframes[i].contentWindow.postMessage(ready, '*'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| })(); | ||||
							
								
								
									
										506
									
								
								src/core/features/h5p/assets/js/h5p-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								src/core/features/h5p/assets/js/h5p-utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,506 @@ | ||||
| /* global H5PAdminIntegration*/ | ||||
| var H5PUtils = H5PUtils || {}; | ||||
| 
 | ||||
| (function ($) { | ||||
|   /** | ||||
|    * Generic function for creating a table including the headers | ||||
|    * | ||||
|    * @param {array} headers List of headers | ||||
|    */ | ||||
|   H5PUtils.createTable = function (headers) { | ||||
|     var $table = $('<table class="h5p-admin-table' + (H5PAdminIntegration.extraTableClasses !== undefined ? ' ' + H5PAdminIntegration.extraTableClasses : '') + '"></table>'); | ||||
| 
 | ||||
|     if (headers) { | ||||
|       var $thead = $('<thead></thead>'); | ||||
|       var $tr = $('<tr></tr>'); | ||||
| 
 | ||||
|       $.each(headers, function (index, value) { | ||||
|         if (!(value instanceof Object)) { | ||||
|           value = { | ||||
|             html: value | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         $('<th/>', value).appendTo($tr); | ||||
|       }); | ||||
| 
 | ||||
|       $table.append($thead.append($tr)); | ||||
|     } | ||||
| 
 | ||||
|     return $table; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Generic function for creating a table row | ||||
|    * | ||||
|    * @param {array} rows Value list. Object name is used as class name in <TD> | ||||
|    */ | ||||
|   H5PUtils.createTableRow = function (rows) { | ||||
|     var $tr = $('<tr></tr>'); | ||||
| 
 | ||||
|     $.each(rows, function (index, value) { | ||||
|       if (!(value instanceof Object)) { | ||||
|         value = { | ||||
|           html: value | ||||
|         }; | ||||
|       } | ||||
| 
 | ||||
|       $('<td/>', value).appendTo($tr); | ||||
|     }); | ||||
| 
 | ||||
|     return $tr; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Generic function for creating a field containing label and value | ||||
|    * | ||||
|    * @param {string} label The label displayed in front of the value | ||||
|    * @param {string} value The value | ||||
|    */ | ||||
|   H5PUtils.createLabeledField = function (label, value) { | ||||
|     var $field = $('<div class="h5p-labeled-field"></div>'); | ||||
| 
 | ||||
|     $field.append('<div class="h5p-label">' + label + '</div>'); | ||||
|     $field.append('<div class="h5p-value">' + value + '</div>'); | ||||
| 
 | ||||
|     return $field; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Replaces placeholder fields in translation strings | ||||
|    * | ||||
|    * @param {string} template The translation template string in the following format: "$name is a $sex" | ||||
|    * @param {array} replacors An js object with key and values. Eg: {'$name': 'Frode', '$sex': 'male'} | ||||
|    */ | ||||
|   H5PUtils.translateReplace = function (template, replacors) { | ||||
|     $.each(replacors, function (key, value) { | ||||
|       template = template.replace(new RegExp('\\'+key, 'g'), value); | ||||
|     }); | ||||
|     return template; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Get throbber with given text. | ||||
|    * | ||||
|    * @param {String} text | ||||
|    * @returns {$} | ||||
|    */ | ||||
|   H5PUtils.throbber = function (text) { | ||||
|     return $('<div/>', { | ||||
|       class: 'h5p-throbber', | ||||
|       text: text | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Makes it possbile to rebuild all content caches from admin UI. | ||||
|    * @param {Object} notCached | ||||
|    * @returns {$} | ||||
|    */ | ||||
|   H5PUtils.getRebuildCache = function (notCached) { | ||||
|     var $container = $('<div class="h5p-admin-rebuild-cache"><p class="message">' + notCached.message + '</p><p class="progress">' + notCached.progress + '</p></div>'); | ||||
|     var $button = $('<button>' + notCached.button + '</button>').appendTo($container).click(function () { | ||||
|       var $spinner = $('<div/>', {class: 'h5p-spinner'}).replaceAll($button); | ||||
|       var parts = ['|', '/', '-', '\\']; | ||||
|       var current = 0; | ||||
|       var spinning = setInterval(function () { | ||||
|         $spinner.text(parts[current]); | ||||
|         current++; | ||||
|         if (current === parts.length) current = 0; | ||||
|       }, 100); | ||||
| 
 | ||||
|       var $counter = $container.find('.progress'); | ||||
|       var build = function () { | ||||
|         $.post(notCached.url, function (left) { | ||||
|           if (left === '0') { | ||||
|             clearInterval(spinning); | ||||
|             $container.remove(); | ||||
|             location.reload(); | ||||
|           } | ||||
|           else { | ||||
|             var counter = $counter.text().split(' '); | ||||
|             counter[0] = left; | ||||
|             $counter.text(counter.join(' ')); | ||||
|             build(); | ||||
|           } | ||||
|         }); | ||||
|       }; | ||||
|       build(); | ||||
|     }); | ||||
| 
 | ||||
|     return $container; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Generic table class with useful helpers. | ||||
|    * | ||||
|    * @class | ||||
|    * @param {Object} classes | ||||
|    *   Custom html classes to use on elements. | ||||
|    *   e.g. {tableClass: 'fixed'}. | ||||
|    */ | ||||
|   H5PUtils.Table = function (classes) { | ||||
|     var numCols; | ||||
|     var sortByCol; | ||||
|     var $sortCol; | ||||
|     var sortCol; | ||||
|     var sortDir; | ||||
| 
 | ||||
|     // Create basic table
 | ||||
|     var tableOptions = {}; | ||||
|     if (classes.table !== undefined) { | ||||
|       tableOptions['class'] = classes.table; | ||||
|     } | ||||
|     var $table = $('<table/>', tableOptions); | ||||
|     var $thead = $('<thead/>').appendTo($table); | ||||
|     var $tfoot = $('<tfoot/>').appendTo($table); | ||||
|     var $tbody = $('<tbody/>').appendTo($table); | ||||
| 
 | ||||
|     /** | ||||
|      * Add columns to given table row. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {jQuery} $tr Table row | ||||
|      * @param {(String|Object)} col Column properties | ||||
|      * @param {Number} id Used to seperate the columns | ||||
|      */ | ||||
|     var addCol = function ($tr, col, id) { | ||||
|       var options = { | ||||
|         on: {} | ||||
|       }; | ||||
| 
 | ||||
|       if (!(col instanceof Object)) { | ||||
|         options.text = col; | ||||
|       } | ||||
|       else { | ||||
|         if (col.text !== undefined) { | ||||
|           options.text = col.text; | ||||
|         } | ||||
|         if (col.class !== undefined) { | ||||
|           options.class = col.class; | ||||
|         } | ||||
| 
 | ||||
|         if (sortByCol !== undefined && col.sortable === true) { | ||||
|           // Make sortable
 | ||||
|           options.role = 'button'; | ||||
|           options.tabIndex = 0; | ||||
| 
 | ||||
|           // This is the first sortable column, use as default sort
 | ||||
|           if (sortCol === undefined) { | ||||
|             sortCol = id; | ||||
|             sortDir = 0; | ||||
|           } | ||||
| 
 | ||||
|           // This is the sort column
 | ||||
|           if (sortCol === id) { | ||||
|             options['class'] = 'h5p-sort'; | ||||
|             if (sortDir === 1) { | ||||
|               options['class'] += ' h5p-reverse'; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           options.on.click = function () { | ||||
|             sort($th, id); | ||||
|           }; | ||||
|           options.on.keypress = function (event) { | ||||
|             if ((event.charCode || event.keyCode) === 32) { // Space
 | ||||
|               sort($th, id); | ||||
|             } | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Append
 | ||||
|       var $th = $('<th>', options).appendTo($tr); | ||||
|       if (sortCol === id) { | ||||
|         $sortCol = $th; // Default sort column
 | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the UI when a column header has been clicked. | ||||
|      * Triggers sorting callback. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {jQuery} $th Table header | ||||
|      * @param {Number} id Used to seperate the columns | ||||
|      */ | ||||
|     var sort = function ($th, id) { | ||||
|       if (id === sortCol) { | ||||
|         // Change sorting direction
 | ||||
|         if (sortDir === 0) { | ||||
|           sortDir = 1; | ||||
|           $th.addClass('h5p-reverse'); | ||||
|         } | ||||
|         else { | ||||
|           sortDir = 0; | ||||
|           $th.removeClass('h5p-reverse'); | ||||
|         } | ||||
|       } | ||||
|       else { | ||||
|         // Change sorting column
 | ||||
|         $sortCol.removeClass('h5p-sort').removeClass('h5p-reverse'); | ||||
|         $sortCol = $th.addClass('h5p-sort'); | ||||
|         sortCol = id; | ||||
|         sortDir = 0; | ||||
|       } | ||||
| 
 | ||||
|       sortByCol({ | ||||
|         by: sortCol, | ||||
|         dir: sortDir | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Set table headers. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {Array} cols | ||||
|      *   Table header data. Can be strings or objects with options like | ||||
|      *   "text" and "sortable". E.g. | ||||
|      *   [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] | ||||
|      * @param {Function} sort Callback which is runned when sorting changes | ||||
|      * @param {Object} [order] | ||||
|      */ | ||||
|     this.setHeaders = function (cols, sort, order) { | ||||
|       numCols = cols.length; | ||||
|       sortByCol = sort; | ||||
| 
 | ||||
|       if (order) { | ||||
|         sortCol = order.by; | ||||
|         sortDir = order.dir; | ||||
|       } | ||||
| 
 | ||||
|       // Create new head
 | ||||
|       var $newThead = $('<thead/>'); | ||||
|       var $tr = $('<tr/>').appendTo($newThead); | ||||
|       for (var i = 0; i < cols.length; i++) { | ||||
|         addCol($tr, cols[i], i); | ||||
|       } | ||||
| 
 | ||||
|       // Update DOM
 | ||||
|       $thead.replaceWith($newThead); | ||||
|       $thead = $newThead; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Set table rows. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {Array} rows Table rows with cols: [[1,'hello',3],[2,'asd',6]] | ||||
|      */ | ||||
|     this.setRows = function (rows) { | ||||
|       var $newTbody = $('<tbody/>'); | ||||
| 
 | ||||
|       for (var i = 0; i < rows.length; i++) { | ||||
|         var $tr = $('<tr/>').appendTo($newTbody); | ||||
| 
 | ||||
|         for (var j = 0; j < rows[i].length; j++) { | ||||
|           $('<td>', { | ||||
|             html: rows[i][j] | ||||
|           }).appendTo($tr); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       $tbody.replaceWith($newTbody); | ||||
|       $tbody = $newTbody; | ||||
| 
 | ||||
|       return $tbody; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Set custom table body content. This can be a message or a throbber. | ||||
|      * Will cover all table columns. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {jQuery} $content Custom content | ||||
|      */ | ||||
|     this.setBody = function ($content) { | ||||
|       var $newTbody = $('<tbody/>'); | ||||
|       var $tr = $('<tr/>').appendTo($newTbody); | ||||
|       $('<td>', { | ||||
|         colspan: numCols | ||||
|       }).append($content).appendTo($tr); | ||||
|       $tbody.replaceWith($newTbody); | ||||
|       $tbody = $newTbody; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Set custom table foot content. This can be a pagination widget. | ||||
|      * Will cover all table columns. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {jQuery} $content Custom content | ||||
|      */ | ||||
|     this.setFoot = function ($content) { | ||||
|       var $newTfoot = $('<tfoot/>'); | ||||
|       var $tr = $('<tr/>').appendTo($newTfoot); | ||||
|       $('<td>', { | ||||
|         colspan: numCols | ||||
|       }).append($content).appendTo($tr); | ||||
|       $tfoot.replaceWith($newTfoot); | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Appends the table to the given container. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {jQuery} $container | ||||
|      */ | ||||
|     this.appendTo = function ($container) { | ||||
|       $table.appendTo($container); | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Generic pagination class. Creates a useful pagination widget. | ||||
|    * | ||||
|    * @class | ||||
|    * @param {Number} num Total number of items to pagiate. | ||||
|    * @param {Number} limit Number of items to dispaly per page. | ||||
|    * @param {Function} goneTo | ||||
|    *   Callback which is fired when the user wants to go to another page. | ||||
|    * @param {Object} l10n | ||||
|    *   Localization / translations. e.g. | ||||
|    *   { | ||||
|    *     currentPage: 'Page $current of $total', | ||||
|    *     nextPage: 'Next page', | ||||
|    *     previousPage: 'Previous page' | ||||
|    *   } | ||||
|    */ | ||||
|   H5PUtils.Pagination = function (num, limit, goneTo, l10n) { | ||||
|     var current = 0; | ||||
|     var pages = Math.ceil(num / limit); | ||||
| 
 | ||||
|     // Create components
 | ||||
| 
 | ||||
|     // Previous button
 | ||||
|     var $left = $('<button/>', { | ||||
|       html: '<', | ||||
|       'class': 'button', | ||||
|       title: l10n.previousPage | ||||
|     }).click(function () { | ||||
|       goTo(current - 1); | ||||
|     }); | ||||
| 
 | ||||
|     // Current page text
 | ||||
|     var $text = $('<span/>').click(function () { | ||||
|       $input.width($text.width()).show().val(current + 1).focus(); | ||||
|       $text.hide(); | ||||
|     }); | ||||
| 
 | ||||
|     // Jump to page input
 | ||||
|     var $input = $('<input/>', { | ||||
|       type: 'number', | ||||
|       min : 1, | ||||
|       max: pages, | ||||
|       on: { | ||||
|         'blur': function () { | ||||
|           gotInput(); | ||||
|         }, | ||||
|         'keyup': function (event) { | ||||
|           if (event.keyCode === 13) { | ||||
|             gotInput(); | ||||
|             return false; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }).hide(); | ||||
| 
 | ||||
|     // Next button
 | ||||
|     var $right = $('<button/>', { | ||||
|       html: '>', | ||||
|       'class': 'button', | ||||
|       title: l10n.nextPage | ||||
|     }).click(function () { | ||||
|       goTo(current + 1); | ||||
|     }); | ||||
| 
 | ||||
|     /** | ||||
|      * Check what page the user has typed in and jump to it. | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     var gotInput = function () { | ||||
|       var page = parseInt($input.hide().val()); | ||||
|       if (!isNaN(page)) { | ||||
|         goTo(page - 1); | ||||
|       } | ||||
|       $text.show(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Update UI elements. | ||||
|      * | ||||
|      * @private | ||||
|      */ | ||||
|     var updateUI = function () { | ||||
|       var next = current + 1; | ||||
| 
 | ||||
|       // Disable or enable buttons
 | ||||
|       $left.attr('disabled', current === 0); | ||||
|       $right.attr('disabled', next === pages); | ||||
| 
 | ||||
|       // Update counter
 | ||||
|       $text.html(l10n.currentPage.replace('$current', next).replace('$total', pages)); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Try to go to the requested page. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {Number} page | ||||
|      */ | ||||
|     var goTo = function (page) { | ||||
|       if (page === current || page < 0 || page >= pages) { | ||||
|         return; // Invalid page number
 | ||||
|       } | ||||
|       current = page; | ||||
| 
 | ||||
|       updateUI(); | ||||
| 
 | ||||
|       // Fire callback
 | ||||
|       goneTo(page * limit); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Update number of items and limit. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {Number} newNum Total number of items to pagiate. | ||||
|      * @param {Number} newLimit Number of items to dispaly per page. | ||||
|      */ | ||||
|     this.update = function (newNum, newLimit) { | ||||
|       if (newNum !== num || newLimit !== limit) { | ||||
|         // Update num and limit
 | ||||
|         num = newNum; | ||||
|         limit = newLimit; | ||||
|         pages = Math.ceil(num / limit); | ||||
|         $input.attr('max', pages); | ||||
| 
 | ||||
|         if (current >= pages) { | ||||
|           // Content is gone, move to last page.
 | ||||
|           goTo(pages - 1); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         updateUI(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Append the pagination widget to the given container. | ||||
|      * | ||||
|      * @public | ||||
|      * @param {jQuery} $container | ||||
|      */ | ||||
|     this.appendTo = function ($container) { | ||||
|       $left.add($text).add($input).add($right).appendTo($container); | ||||
|     }; | ||||
| 
 | ||||
|     // Update UI
 | ||||
|     updateUI(); | ||||
|   }; | ||||
| 
 | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										40
									
								
								src/core/features/h5p/assets/js/h5p-version.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/core/features/h5p/assets/js/h5p-version.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| H5P.Version = (function () { | ||||
|   /** | ||||
|    * Make it easy to keep track of version details. | ||||
|    * | ||||
|    * @class | ||||
|    * @namespace H5P | ||||
|    * @param {String} version | ||||
|    */ | ||||
|   function Version(version) { | ||||
| 
 | ||||
|     if (typeof version === 'string') { | ||||
|       // Name version string (used by content upgrade)
 | ||||
|       var versionSplit = version.split('.', 3); | ||||
|       this.major =+ versionSplit[0]; | ||||
|       this.minor =+ versionSplit[1]; | ||||
|     } | ||||
|     else { | ||||
|       // Library objects (used by editor)
 | ||||
|       if (version.localMajorVersion !== undefined) { | ||||
|         this.major =+ version.localMajorVersion; | ||||
|         this.minor =+ version.localMinorVersion; | ||||
|       } | ||||
|       else { | ||||
|         this.major =+ version.majorVersion; | ||||
|         this.minor =+ version.minorVersion; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Public. Custom string for this object. | ||||
|      * | ||||
|      * @returns {String} | ||||
|      */ | ||||
|     this.toString = function () { | ||||
|       return version; | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return Version; | ||||
| })(); | ||||
							
								
								
									
										331
									
								
								src/core/features/h5p/assets/js/h5p-x-api-event.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								src/core/features/h5p/assets/js/h5p-x-api-event.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,331 @@ | ||||
| var H5P = window.H5P = window.H5P || {}; | ||||
| 
 | ||||
| /** | ||||
|  * Used for xAPI events. | ||||
|  * | ||||
|  * @class | ||||
|  * @extends H5P.Event | ||||
|  */ | ||||
| H5P.XAPIEvent = function () { | ||||
|   H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); | ||||
| }; | ||||
| 
 | ||||
| H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); | ||||
| H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; | ||||
| 
 | ||||
| /** | ||||
|  * Set scored result statements. | ||||
|  * | ||||
|  * @param {number} score | ||||
|  * @param {number} maxScore | ||||
|  * @param {object} instance | ||||
|  * @param {boolean} completion | ||||
|  * @param {boolean} success | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.setScoredResult = function (score, maxScore, instance, completion, success) { | ||||
|   this.data.statement.result = {}; | ||||
| 
 | ||||
|   if (typeof score !== 'undefined') { | ||||
|     if (typeof maxScore === 'undefined') { | ||||
|       this.data.statement.result.score = {'raw': score}; | ||||
|     } | ||||
|     else { | ||||
|       this.data.statement.result.score = { | ||||
|         'min': 0, | ||||
|         'max': maxScore, | ||||
|         'raw': score | ||||
|       }; | ||||
|       if (maxScore > 0) { | ||||
|         this.data.statement.result.score.scaled = Math.round(score / maxScore * 10000) / 10000; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (typeof completion === 'undefined') { | ||||
|     this.data.statement.result.completion = (this.getVerb() === 'completed' || this.getVerb() === 'answered'); | ||||
|   } | ||||
|   else { | ||||
|     this.data.statement.result.completion = completion; | ||||
|   } | ||||
| 
 | ||||
|   if (typeof success !== 'undefined') { | ||||
|     this.data.statement.result.success = success; | ||||
|   } | ||||
| 
 | ||||
|   if (instance && instance.activityStartTime) { | ||||
|     var duration = Math.round((Date.now() - instance.activityStartTime ) / 10) / 100; | ||||
|     // xAPI spec allows a precision of 0.01 seconds
 | ||||
| 
 | ||||
|     this.data.statement.result.duration = 'PT' + duration + 'S'; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set a verb. | ||||
|  * | ||||
|  * @param {string} verb | ||||
|  *   Verb in short form, one of the verbs defined at | ||||
|  *   {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary}
 | ||||
|  * | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.setVerb = function (verb) { | ||||
|   if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { | ||||
|     this.data.statement.verb = { | ||||
|       'id': 'http://adlnet.gov/expapi/verbs/' + verb, | ||||
|       'display': { | ||||
|         'en-US': verb | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|   else if (verb.id !== undefined) { | ||||
|     this.data.statement.verb = verb; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get the statements verb id. | ||||
|  * | ||||
|  * @param {boolean} full | ||||
|  *   if true the full verb id prefixed by http://adlnet.gov/expapi/verbs/
 | ||||
|  *   will be returned | ||||
|  * @returns {string} | ||||
|  *   Verb or null if no verb with an id has been defined | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.getVerb = function (full) { | ||||
|   var statement = this.data.statement; | ||||
|   if ('verb' in statement) { | ||||
|     if (full === true) { | ||||
|       return statement.verb; | ||||
|     } | ||||
|     return statement.verb.id.slice(31); | ||||
|   } | ||||
|   else { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set the object part of the statement. | ||||
|  * | ||||
|  * The id is found automatically (the url to the content) | ||||
|  * | ||||
|  * @param {Object} instance | ||||
|  *   The H5P instance | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.setObject = function (instance) { | ||||
|   if (instance.contentId) { | ||||
|     this.data.statement.object = { | ||||
|       'id': this.getContentXAPIId(instance), | ||||
|       'objectType': 'Activity', | ||||
|       'definition': { | ||||
|         'extensions': { | ||||
|           'http://h5p.org/x-api/h5p-local-content-id': instance.contentId | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|     if (instance.subContentId) { | ||||
|       this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId; | ||||
|       // Don't set titles on main content, title should come from publishing platform
 | ||||
|       if (typeof instance.getTitle === 'function') { | ||||
|         this.data.statement.object.definition.name = { | ||||
|           "en-US": instance.getTitle() | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
|       var content = H5P.getContentForInstance(instance.contentId); | ||||
|       if (content && content.metadata && content.metadata.title) { | ||||
|         this.data.statement.object.definition.name = { | ||||
|           "en-US": H5P.createTitle(content.metadata.title) | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   else { | ||||
|     // Content types view always expect to have a contentId when they are displayed.
 | ||||
|     // This is not the case if they are displayed in the editor as part of a preview.
 | ||||
|     // The fix is to set an empty object with definition for the xAPI event, so all
 | ||||
|     // the content types that rely on this does not have to handle it. This means
 | ||||
|     // that content types that are being previewed will send xAPI completed events,
 | ||||
|     // but since there are no scripts that catch these events in the editor,
 | ||||
|     // this is not a problem.
 | ||||
|     this.data.statement.object = { | ||||
|       definition: {} | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set the context part of the statement. | ||||
|  * | ||||
|  * @param {Object} instance | ||||
|  *   The H5P instance | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.setContext = function (instance) { | ||||
|   if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) { | ||||
|     this.data.statement.context = { | ||||
|       "contextActivities": { | ||||
|         "parent": [ | ||||
|           { | ||||
|             "id": this.getContentXAPIId(instance.parent), | ||||
|             "objectType": "Activity" | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|   if (instance.libraryInfo) { | ||||
|     if (this.data.statement.context === undefined) { | ||||
|       this.data.statement.context = {"contextActivities":{}}; | ||||
|     } | ||||
|     this.data.statement.context.contextActivities.category = [ | ||||
|       { | ||||
|         "id": "http://h5p.org/libraries/" + instance.libraryInfo.versionedNameNoSpaces, | ||||
|         "objectType": "Activity" | ||||
|       } | ||||
|     ]; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set the actor. Email and name will be added automatically. | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.setActor = function () { | ||||
|   if (H5PIntegration.user !== undefined) { | ||||
|     this.data.statement.actor = { | ||||
|       'name': H5PIntegration.user.name, | ||||
|       'mbox': 'mailto:' + H5PIntegration.user.mail, | ||||
|       'objectType': 'Agent' | ||||
|     }; | ||||
|   } | ||||
|   else { | ||||
|     var uuid; | ||||
|     try { | ||||
|       if (localStorage.H5PUserUUID) { | ||||
|         uuid = localStorage.H5PUserUUID; | ||||
|       } | ||||
|       else { | ||||
|         uuid = H5P.createUUID(); | ||||
|         localStorage.H5PUserUUID = uuid; | ||||
|       } | ||||
|     } | ||||
|     catch (err) { | ||||
|       // LocalStorage and Cookies are probably disabled. Do not track the user.
 | ||||
|       uuid = 'not-trackable-' + H5P.createUUID(); | ||||
|     } | ||||
|     this.data.statement.actor = { | ||||
|       'account': { | ||||
|         'name': uuid, | ||||
|         'homePage': H5PIntegration.siteUrl | ||||
|       }, | ||||
|       'objectType': 'Agent' | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get the max value of the result - score part of the statement | ||||
|  * | ||||
|  * @returns {number} | ||||
|  *   The max score, or null if not defined | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.getMaxScore = function () { | ||||
|   return this.getVerifiedStatementValue(['result', 'score', 'max']); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get the raw value of the result - score part of the statement | ||||
|  * | ||||
|  * @returns {number} | ||||
|  *   The score, or null if not defined | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.getScore = function () { | ||||
|   return this.getVerifiedStatementValue(['result', 'score', 'raw']); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get content xAPI ID. | ||||
|  * | ||||
|  * @param {Object} instance | ||||
|  *   The H5P instance | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { | ||||
|   var xAPIId; | ||||
|   if (instance.contentId && H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId]) { | ||||
|     xAPIId =  H5PIntegration.contents['cid-' + instance.contentId].url; | ||||
|     if (instance.subContentId) { | ||||
|       xAPIId += '?subContentId=' +  instance.subContentId; | ||||
|     } | ||||
|   } | ||||
|   return xAPIId; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Check if this event is sent from a child (i.e not from grandchild) | ||||
|  * | ||||
|  * @return {Boolean} | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.isFromChild = function () { | ||||
|   var parentId = this.getVerifiedStatementValue(['context', 'contextActivities', 'parent', 0, 'id']); | ||||
|   return !parentId || parentId.indexOf('subContentId') === -1; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Figure out if a property exists in the statement and return it | ||||
|  * | ||||
|  * @param {string[]} keys | ||||
|  *   List describing the property we're looking for. For instance | ||||
|  *   ['result', 'score', 'raw'] for result.score.raw | ||||
|  * @returns {*} | ||||
|  *   The value of the property if it is set, null otherwise. | ||||
|  */ | ||||
| H5P.XAPIEvent.prototype.getVerifiedStatementValue = function (keys) { | ||||
|   var val = this.data.statement; | ||||
|   for (var i = 0; i < keys.length; i++) { | ||||
|     if (val[keys[i]] === undefined) { | ||||
|       return null; | ||||
|     } | ||||
|     val = val[keys[i]]; | ||||
|   } | ||||
|   return val; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * List of verbs defined at {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary}
 | ||||
|  * | ||||
|  * @type Array | ||||
|  */ | ||||
| H5P.XAPIEvent.allowedXAPIVerbs = [ | ||||
|   'answered', | ||||
|   'asked', | ||||
|   'attempted', | ||||
|   'attended', | ||||
|   'commented', | ||||
|   'completed', | ||||
|   'exited', | ||||
|   'experienced', | ||||
|   'failed', | ||||
|   'imported', | ||||
|   'initialized', | ||||
|   'interacted', | ||||
|   'launched', | ||||
|   'mastered', | ||||
|   'passed', | ||||
|   'preferred', | ||||
|   'progressed', | ||||
|   'registered', | ||||
|   'responded', | ||||
|   'resumed', | ||||
|   'scored', | ||||
|   'shared', | ||||
|   'suspended', | ||||
|   'terminated', | ||||
|   'voided', | ||||
| 
 | ||||
|   // Custom verbs used for action toolbar below content
 | ||||
|   'downloaded', | ||||
|   'copied', | ||||
|   'accessed-reuse', | ||||
|   'accessed-embed', | ||||
|   'accessed-copyright' | ||||
| ]; | ||||
							
								
								
									
										119
									
								
								src/core/features/h5p/assets/js/h5p-x-api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/core/features/h5p/assets/js/h5p-x-api.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| var H5P = window.H5P = window.H5P || {}; | ||||
| 
 | ||||
| /** | ||||
|  * The external event dispatcher. Others, outside of H5P may register and | ||||
|  * listen for H5P Events here. | ||||
|  * | ||||
|  * @type {H5P.EventDispatcher} | ||||
|  */ | ||||
| H5P.externalDispatcher = new H5P.EventDispatcher(); | ||||
| 
 | ||||
| // EventDispatcher extensions
 | ||||
| 
 | ||||
| /** | ||||
|  * Helper function for triggering xAPI added to the EventDispatcher. | ||||
|  * | ||||
|  * @param {string} verb | ||||
|  *   The short id of the verb we want to trigger | ||||
|  * @param {Oject} [extra] | ||||
|  *   Extra properties for the xAPI statement | ||||
|  */ | ||||
| H5P.EventDispatcher.prototype.triggerXAPI = function (verb, extra) { | ||||
|   this.trigger(this.createXAPIEventTemplate(verb, extra)); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Helper function to create event templates added to the EventDispatcher. | ||||
|  * | ||||
|  * Will in the future be used to add representations of the questions to the | ||||
|  * statements. | ||||
|  * | ||||
|  * @param {string} verb | ||||
|  *   Verb id in short form | ||||
|  * @param {Object} [extra] | ||||
|  *   Extra values to be added to the statement | ||||
|  * @returns {H5P.XAPIEvent} | ||||
|  *   Instance | ||||
|  */ | ||||
| H5P.EventDispatcher.prototype.createXAPIEventTemplate = function (verb, extra) { | ||||
|   var event = new H5P.XAPIEvent(); | ||||
| 
 | ||||
|   event.setActor(); | ||||
|   event.setVerb(verb); | ||||
|   if (extra !== undefined) { | ||||
|     for (var i in extra) { | ||||
|       event.data.statement[i] = extra[i]; | ||||
|     } | ||||
|   } | ||||
|   if (!('object' in event.data.statement)) { | ||||
|     event.setObject(this); | ||||
|   } | ||||
|   if (!('context' in event.data.statement)) { | ||||
|     event.setContext(this); | ||||
|   } | ||||
|   return event; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Helper function to create xAPI completed events | ||||
|  * | ||||
|  * DEPRECATED - USE triggerXAPIScored instead | ||||
|  * | ||||
|  * @deprecated | ||||
|  *   since 1.5, use triggerXAPIScored instead. | ||||
|  * @param {number} score | ||||
|  *   Will be set as the 'raw' value of the score object | ||||
|  * @param {number} maxScore | ||||
|  *   will be set as the "max" value of the score object | ||||
|  * @param {boolean} success | ||||
|  *   will be set as the "success" value of the result object | ||||
|  */ | ||||
| H5P.EventDispatcher.prototype.triggerXAPICompleted = function (score, maxScore, success) { | ||||
|   this.triggerXAPIScored(score, maxScore, 'completed', true, success); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Helper function to create scored xAPI events | ||||
|  * | ||||
|  * @param {number} score | ||||
|  *   Will be set as the 'raw' value of the score object | ||||
|  * @param {number} maxScore | ||||
|  *   Will be set as the "max" value of the score object | ||||
|  * @param {string} verb | ||||
|  *   Short form of adl verb | ||||
|  * @param {boolean} completion | ||||
|  *   Is this a statement from a completed activity? | ||||
|  * @param {boolean} success | ||||
|  *   Is this a statement from an activity that was done successfully? | ||||
|  */ | ||||
| H5P.EventDispatcher.prototype.triggerXAPIScored = function (score, maxScore, verb, completion, success) { | ||||
|   var event = this.createXAPIEventTemplate(verb); | ||||
|   event.setScoredResult(score, maxScore, this, completion, success); | ||||
|   this.trigger(event); | ||||
| }; | ||||
| 
 | ||||
| H5P.EventDispatcher.prototype.setActivityStarted = function () { | ||||
|   if (this.activityStartTime === undefined) { | ||||
|     // Don't trigger xAPI events in the editor
 | ||||
|     if (this.contentId !== undefined && | ||||
|         H5PIntegration.contents !== undefined && | ||||
|         H5PIntegration.contents['cid-' + this.contentId] !== undefined) { | ||||
|       this.triggerXAPI('attempted'); | ||||
|     } | ||||
|     this.activityStartTime = Date.now(); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Internal H5P function listening for xAPI completed events and stores scores | ||||
|  * | ||||
|  * @param {H5P.XAPIEvent} event | ||||
|  */ | ||||
| H5P.xAPICompletedListener = function (event) { | ||||
|   if ((event.getVerb() === 'completed' || event.getVerb() === 'answered') && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) { | ||||
|     var score = event.getScore(); | ||||
|     var maxScore = event.getMaxScore(); | ||||
|     var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); | ||||
|     H5P.setFinished(contentId, score, maxScore); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										2847
									
								
								src/core/features/h5p/assets/js/h5p.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2847
									
								
								src/core/features/h5p/assets/js/h5p.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										13
									
								
								src/core/features/h5p/assets/js/jquery.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/core/features/h5p/assets/js/jquery.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										436
									
								
								src/core/features/h5p/assets/js/request-queue.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								src/core/features/h5p/assets/js/request-queue.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,436 @@ | ||||
| /** | ||||
|  * Queue requests and handle them at your convenience | ||||
|  * | ||||
|  * @type {RequestQueue} | ||||
|  */ | ||||
| H5P.RequestQueue = (function ($, EventDispatcher) { | ||||
|   /** | ||||
|    * A queue for requests, will be automatically processed when regaining connection | ||||
|    * | ||||
|    * @param {boolean} [options.showToast] Show toast when losing or regaining connection | ||||
|    * @constructor | ||||
|    */ | ||||
|   const RequestQueue = function (options) { | ||||
|     EventDispatcher.call(this); | ||||
|     this.processingQueue = false; | ||||
|     options = options || {}; | ||||
| 
 | ||||
|     this.showToast = options.showToast; | ||||
|     this.itemName = 'requestQueue'; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Add request to queue. Only supports posts currently. | ||||
|    * | ||||
|    * @param {string} url | ||||
|    * @param {Object} data | ||||
|    * @returns {boolean} | ||||
|    */ | ||||
|   RequestQueue.prototype.add = function (url, data) { | ||||
|     if (!window.localStorage) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     let storedStatements = this.getStoredRequests(); | ||||
|     if (!storedStatements) { | ||||
|       storedStatements = []; | ||||
|     } | ||||
| 
 | ||||
|     storedStatements.push({ | ||||
|       url: url, | ||||
|       data: data, | ||||
|     }); | ||||
| 
 | ||||
|     window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements)); | ||||
| 
 | ||||
|     this.trigger('requestQueued', { | ||||
|       storedStatements: storedStatements, | ||||
|       processingQueue: this.processingQueue, | ||||
|     }); | ||||
|     return true; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Get stored requests | ||||
|    * | ||||
|    * @returns {boolean|Array} Stored requests | ||||
|    */ | ||||
|   RequestQueue.prototype.getStoredRequests = function () { | ||||
|     if (!window.localStorage) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     const item = window.localStorage.getItem(this.itemName); | ||||
|     if (!item) { | ||||
|       return []; | ||||
|     } | ||||
| 
 | ||||
|     return JSON.parse(item); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Clear stored requests | ||||
|    * | ||||
|    * @returns {boolean} True if the storage was successfully cleared | ||||
|    */ | ||||
|   RequestQueue.prototype.clearQueue = function () { | ||||
|     if (!window.localStorage) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     window.localStorage.removeItem(this.itemName); | ||||
|     return true; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Start processing of requests queue | ||||
|    * | ||||
|    * @return {boolean} Returns false if it was not possible to resume processing queue | ||||
|    */ | ||||
|   RequestQueue.prototype.resumeQueue = function () { | ||||
|     // Not supported
 | ||||
|     if (!H5PIntegration || !window.navigator || !window.localStorage) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     // Already processing
 | ||||
|     if (this.processingQueue) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     // Attempt to send queued requests
 | ||||
|     const queue = this.getStoredRequests(); | ||||
|     const queueLength = queue.length; | ||||
| 
 | ||||
|     // Clear storage, failed requests will be re-added
 | ||||
|     this.clearQueue(); | ||||
| 
 | ||||
|     // No items left in queue
 | ||||
|     if (!queueLength) { | ||||
|       this.trigger('emptiedQueue', queue); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     // Make sure requests are not changed while they're being handled
 | ||||
|     this.processingQueue = true; | ||||
| 
 | ||||
|     // Process queue in original order
 | ||||
|     this.processQueue(queue); | ||||
|     return true | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Process first item in the request queue | ||||
|    * | ||||
|    * @param {Array} queue Request queue | ||||
|    */ | ||||
|   RequestQueue.prototype.processQueue = function (queue) { | ||||
|     if (!queue.length) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.trigger('processingQueue'); | ||||
| 
 | ||||
|     // Make sure the requests are processed in a FIFO order
 | ||||
|     const request = queue.shift(); | ||||
| 
 | ||||
|     const self = this; | ||||
|     $.post(request.url, request.data) | ||||
|       .fail(self.onQueuedRequestFail.bind(self, request)) | ||||
|       .always(self.onQueuedRequestProcessed.bind(self, queue)) | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Request fail handler | ||||
|    * | ||||
|    * @param {Object} request | ||||
|    */ | ||||
|   RequestQueue.prototype.onQueuedRequestFail = function (request) { | ||||
|     // Queue the failed request again if we're offline
 | ||||
|     if (!window.navigator.onLine) { | ||||
|       this.add(request.url, request.data); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * An item in the queue was processed | ||||
|    * | ||||
|    * @param {Array} queue Queue that was processed | ||||
|    */ | ||||
|   RequestQueue.prototype.onQueuedRequestProcessed = function (queue) { | ||||
|     if (queue.length) { | ||||
|       this.processQueue(queue); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Finished processing this queue
 | ||||
|     this.processingQueue = false; | ||||
| 
 | ||||
|     // Run empty queue callback with next request queue
 | ||||
|     const requestQueue = this.getStoredRequests(); | ||||
|     this.trigger('queueEmptied', requestQueue); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Display toast message on the first content of current page | ||||
|    * | ||||
|    * @param {string} msg Message to display | ||||
|    * @param {boolean} [forceShow] Force override showing the toast | ||||
|    * @param {Object} [configOverride] Override toast message config | ||||
|    */ | ||||
|   RequestQueue.prototype.displayToastMessage = function (msg, forceShow, configOverride) { | ||||
|     if (!this.showToast && !forceShow) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const config = H5P.jQuery.extend(true, {}, { | ||||
|       position: { | ||||
|         horizontal : 'centered', | ||||
|         vertical: 'centered', | ||||
|         noOverflowX: true, | ||||
|       } | ||||
|     }, configOverride); | ||||
| 
 | ||||
|     H5P.attachToastTo(H5P.jQuery('.h5p-content:first')[0], msg, config); | ||||
|   }; | ||||
| 
 | ||||
|   return RequestQueue; | ||||
| })(H5P.jQuery, H5P.EventDispatcher); | ||||
| 
 | ||||
| /** | ||||
|  * Request queue for retrying failing requests, will automatically retry them when you come online | ||||
|  * | ||||
|  * @type {offlineRequestQueue} | ||||
|  */ | ||||
| H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) { | ||||
| 
 | ||||
|   /** | ||||
|    * Constructor | ||||
|    * | ||||
|    * @param {Object} [options] Options for offline request queue | ||||
|    * @param {Object} [options.instance] The H5P instance which UI components are placed within | ||||
|    */ | ||||
|   const offlineRequestQueue = function (options) { | ||||
|     const requestQueue = new RequestQueue(); | ||||
| 
 | ||||
|     // We could handle requests from previous pages here, but instead we throw them away
 | ||||
|     requestQueue.clearQueue(); | ||||
| 
 | ||||
|     let startTime = null; | ||||
|     const retryIntervals = [10, 20, 40, 60, 120, 300, 600]; | ||||
|     let intervalIndex = -1; | ||||
|     let currentInterval = null; | ||||
|     let isAttached = false; | ||||
|     let isShowing = false; | ||||
|     let isLoading = false; | ||||
|     const instance = options.instance; | ||||
| 
 | ||||
|     const offlineDialog = new Dialog({ | ||||
|       headerText: H5P.t('offlineDialogHeader'), | ||||
|       dialogText: H5P.t('offlineDialogBody'), | ||||
|       confirmText: H5P.t('offlineDialogRetryButtonLabel'), | ||||
|       hideCancel: true, | ||||
|       hideExit: true, | ||||
|       classes: ['offline'], | ||||
|       instance: instance, | ||||
|       skipRestoreFocus: true, | ||||
|     }); | ||||
| 
 | ||||
|     const dialog = offlineDialog.getElement(); | ||||
| 
 | ||||
|     // Add retry text to body
 | ||||
|     const countDownText = document.createElement('div'); | ||||
|     countDownText.classList.add('count-down'); | ||||
|     countDownText.innerHTML = H5P.t('offlineDialogRetryMessage') | ||||
|       .replace(':num', '<span class="count-down-num">0</span>'); | ||||
| 
 | ||||
|     dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText); | ||||
|     const countDownNum = countDownText.querySelector('.count-down-num'); | ||||
| 
 | ||||
|     // Create throbber
 | ||||
|     const throbberWrapper = document.createElement('div'); | ||||
|     throbberWrapper.classList.add('throbber-wrapper'); | ||||
|     const throbber = document.createElement('div'); | ||||
|     throbber.classList.add('sending-requests-throbber'); | ||||
|     throbberWrapper.appendChild(throbber); | ||||
| 
 | ||||
|     requestQueue.on('requestQueued', function (e) { | ||||
|       // Already processing queue, wait until queue has finished processing before showing dialog
 | ||||
|       if (e.data && e.data.processingQueue) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!isAttached) { | ||||
|         const rootContent = document.body.querySelector('.h5p-content'); | ||||
|         if (!rootContent) { | ||||
|           return; | ||||
|         } | ||||
|         offlineDialog.appendTo(rootContent); | ||||
|         rootContent.appendChild(throbberWrapper); | ||||
|         isAttached = true; | ||||
|       } | ||||
| 
 | ||||
|       startCountDown(); | ||||
|     }.bind(this)); | ||||
| 
 | ||||
|     requestQueue.on('queueEmptied', function (e) { | ||||
|       if (e.data && e.data.length) { | ||||
|         // New requests were added while processing queue or requests failed again. Re-queue requests.
 | ||||
|         startCountDown(true); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // Successfully emptied queue
 | ||||
|       clearInterval(currentInterval); | ||||
|       toggleThrobber(false); | ||||
|       intervalIndex = -1; | ||||
|       if (isShowing) { | ||||
|         offlineDialog.hide(); | ||||
|         isShowing = false; | ||||
|       } | ||||
| 
 | ||||
|       requestQueue.displayToastMessage( | ||||
|         H5P.t('offlineSuccessfulSubmit'), | ||||
|         true, | ||||
|         { | ||||
|           position: { | ||||
|             vertical: 'top', | ||||
|             offsetVertical: '100', | ||||
|           } | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|     }.bind(this)); | ||||
| 
 | ||||
|     offlineDialog.on('confirmed', function () { | ||||
|       // Show dialog on next render in case it is being hidden by the 'confirm' button
 | ||||
|       isShowing = false; | ||||
|       setTimeout(function () { | ||||
|         retryRequests(); | ||||
|       }, 100); | ||||
|     }.bind(this)); | ||||
| 
 | ||||
|     // Initialize listener for when requests are added to queue
 | ||||
|     window.addEventListener('online', function () { | ||||
|       retryRequests(); | ||||
|     }.bind(this)); | ||||
| 
 | ||||
|     // Listen for queued requests outside the iframe
 | ||||
|     window.addEventListener('message', function (event) { | ||||
|       const isValidQueueEvent = window.parent === event.source | ||||
|         && event.data.context === 'h5p' | ||||
|         && event.data.action === 'queueRequest'; | ||||
| 
 | ||||
|       if (!isValidQueueEvent) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       this.add(event.data.url, event.data.data); | ||||
|     }.bind(this)); | ||||
| 
 | ||||
|     /** | ||||
|      * Toggle throbber visibility | ||||
|      * | ||||
|      * @param {boolean} [forceShow] Will force throbber visibility if set | ||||
|      */ | ||||
|     const toggleThrobber = function (forceShow) { | ||||
|       isLoading = !isLoading; | ||||
|       if (forceShow !== undefined) { | ||||
|         isLoading = forceShow; | ||||
|       } | ||||
| 
 | ||||
|       if (isLoading && isShowing) { | ||||
|         offlineDialog.hide(); | ||||
|         isShowing = false; | ||||
|       } | ||||
| 
 | ||||
|       if (isLoading) { | ||||
|         throbberWrapper.classList.add('show'); | ||||
|       } | ||||
|       else { | ||||
|         throbberWrapper.classList.remove('show'); | ||||
|       } | ||||
|     }; | ||||
|     /** | ||||
|      * Retries the failed requests | ||||
|      */ | ||||
|     const retryRequests = function () { | ||||
|       clearInterval(currentInterval); | ||||
|       toggleThrobber(true); | ||||
|       requestQueue.resumeQueue(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Increments retry interval | ||||
|      */ | ||||
|     const incrementRetryInterval = function () { | ||||
|       intervalIndex += 1; | ||||
|       if (intervalIndex >= retryIntervals.length) { | ||||
|         intervalIndex = retryIntervals.length - 1; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Starts counting down to retrying queued requests. | ||||
|      * | ||||
|      * @param forceDelayedShow | ||||
|      */ | ||||
|     const startCountDown = function (forceDelayedShow) { | ||||
|       // Already showing, wait for retry
 | ||||
|       if (isShowing) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       toggleThrobber(false); | ||||
|       if (!isShowing) { | ||||
|         if (forceDelayedShow) { | ||||
|           // Must force delayed show since dialog may be hiding, and confirmation dialog does not
 | ||||
|           //  support this.
 | ||||
|           setTimeout(function () { | ||||
|             offlineDialog.show(0); | ||||
|           }, 100); | ||||
|         } | ||||
|         else { | ||||
|           offlineDialog.show(0); | ||||
|         } | ||||
|       } | ||||
|       isShowing = true; | ||||
|       startTime = new Date().getTime(); | ||||
|       incrementRetryInterval(); | ||||
|       clearInterval(currentInterval); | ||||
|       currentInterval = setInterval(updateCountDown, 100); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the count down timer. Retries requests when time expires. | ||||
|      */ | ||||
|     const updateCountDown = function () { | ||||
|       const time = new Date().getTime(); | ||||
|       const timeElapsed = Math.floor((time - startTime) / 1000); | ||||
|       const timeLeft = retryIntervals[intervalIndex] - timeElapsed; | ||||
|       countDownNum.textContent = timeLeft.toString(); | ||||
| 
 | ||||
|       // Retry interval reached, retry requests
 | ||||
|       if (timeLeft <= 0) { | ||||
|         retryRequests(); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Add request to offline request queue. Only supports posts for now. | ||||
|      * | ||||
|      * @param {string} url The request url | ||||
|      * @param {Object} data The request data | ||||
|      */ | ||||
|     this.add = function (url, data) { | ||||
|       // Only queue request if it failed because we are offline
 | ||||
|       if (window.navigator.onLine) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       requestQueue.add(url, data); | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   return offlineRequestQueue; | ||||
| })(H5P.RequestQueue, H5P.ConfirmationDialog); | ||||
							
								
								
									
										68
									
								
								src/core/features/h5p/assets/js/settings/h5p-disable-hub.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/core/features/h5p/assets/js/settings/h5p-disable-hub.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| /* global H5PDisableHubData */ | ||||
| 
 | ||||
| /** | ||||
|  * Global data for disable hub functionality | ||||
|  * | ||||
|  * @typedef {object} H5PDisableHubData Data passed in from the backend | ||||
|  * | ||||
|  * @property {string} selector Selector for the disable hub check-button | ||||
|  * @property {string} overlaySelector Selector for the element that the confirmation dialog will mask | ||||
|  * @property {Array} errors Errors found with the current server setup | ||||
|  * | ||||
|  * @property {string} header Header of the confirmation dialog | ||||
|  * @property {string} confirmationDialogMsg Body of the confirmation dialog | ||||
|  * @property {string} cancelLabel Cancel label of the confirmation dialog | ||||
|  * @property {string} confirmLabel Confirm button label of the confirmation dialog | ||||
|  * | ||||
|  */ | ||||
| /** | ||||
|  * Utility that makes it possible to force the user to confirm that he really | ||||
|  * wants to use the H5P hub without proper server settings. | ||||
|  */ | ||||
| (function ($) { | ||||
| 
 | ||||
|   $(document).on('ready', function () { | ||||
| 
 | ||||
|     // No data found
 | ||||
|     if (!H5PDisableHubData) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // No errors found, no need for confirmation dialog
 | ||||
|     if (!H5PDisableHubData.errors || !H5PDisableHubData.errors.length) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     H5PDisableHubData.selector = H5PDisableHubData.selector || | ||||
|       '.h5p-settings-disable-hub-checkbox'; | ||||
|     H5PDisableHubData.overlaySelector = H5PDisableHubData.overlaySelector || | ||||
|       '.h5p-settings-container'; | ||||
| 
 | ||||
|     var dialogHtml = '<div>' + | ||||
|       '<p>' + H5PDisableHubData.errors.join('</p><p>') + '</p>' + | ||||
|       '<p>' + H5PDisableHubData.confirmationDialogMsg + '</p>'; | ||||
| 
 | ||||
|     // Create confirmation dialog, make sure to include translations
 | ||||
|     var confirmationDialog = new H5P.ConfirmationDialog({ | ||||
|       headerText: H5PDisableHubData.header, | ||||
|       dialogText: dialogHtml, | ||||
|       cancelText: H5PDisableHubData.cancelLabel, | ||||
|       confirmText: H5PDisableHubData.confirmLabel | ||||
|     }).appendTo($(H5PDisableHubData.overlaySelector).get(0)); | ||||
| 
 | ||||
|     confirmationDialog.on('confirmed', function () { | ||||
|       enableButton.get(0).checked = true; | ||||
|     }); | ||||
| 
 | ||||
|     confirmationDialog.on('canceled', function () { | ||||
|       enableButton.get(0).checked = false; | ||||
|     }); | ||||
| 
 | ||||
|     var enableButton = $(H5PDisableHubData.selector); | ||||
|     enableButton.change(function () { | ||||
|       if ($(this).is(':checked')) { | ||||
|         confirmationDialog.show(enableButton.offset().top); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| })(H5P.jQuery); | ||||
							
								
								
									
										203
									
								
								src/core/features/h5p/assets/moodle/js/embed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/core/features/h5p/assets/moodle/js/embed.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | ||||
| // This file is part of Moodle - http://moodle.org/
 | ||||
| //
 | ||||
| // Moodle is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| //
 | ||||
| // Moodle is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| //
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| /* global H5PEmbedCommunicator:true */ | ||||
| /** | ||||
|  * When embedded the communicator helps talk to the parent page. | ||||
|  * This is a copy of the H5P.communicator, which we need to communicate in this context | ||||
|  * | ||||
|  * @type {H5PEmbedCommunicator} | ||||
|  * @module     core_h5p | ||||
|  * @copyright  2019 Joubel AS <contact@joubel.com> | ||||
|  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | ||||
|  */ | ||||
| H5PEmbedCommunicator = (function() { | ||||
|     /** | ||||
|      * @class | ||||
|      * @private | ||||
|      */ | ||||
|     function Communicator() { | ||||
|         var self = this; | ||||
| 
 | ||||
|         // Maps actions to functions.
 | ||||
|         var actionHandlers = {}; | ||||
| 
 | ||||
|         // Register message listener.
 | ||||
|         window.addEventListener('message', function receiveMessage(event) { | ||||
|             if (window.parent !== event.source || event.data.context !== 'h5p') { | ||||
|                 return; // Only handle messages from parent and in the correct context.
 | ||||
|             } | ||||
| 
 | ||||
|             if (actionHandlers[event.data.action] !== undefined) { | ||||
|                 actionHandlers[event.data.action](event.data); | ||||
|             } | ||||
|         }, false); | ||||
| 
 | ||||
|         /** | ||||
|          * Register action listener. | ||||
|          * | ||||
|          * @param {string} action What you are waiting for | ||||
|          * @param {function} handler What you want done | ||||
|          */ | ||||
|         self.on = function(action, handler) { | ||||
|             actionHandlers[action] = handler; | ||||
|         }; | ||||
| 
 | ||||
|         /** | ||||
|          * Send a message to the all mighty father. | ||||
|          * | ||||
|          * @param {string} action | ||||
|          * @param {Object} [data] payload | ||||
|          */ | ||||
|         self.send = function(action, data) { | ||||
|             if (data === undefined) { | ||||
|                 data = {}; | ||||
|             } | ||||
|             data.context = 'h5p'; | ||||
|             data.action = action; | ||||
| 
 | ||||
|             // Parent origin can be anything.
 | ||||
|             window.parent.postMessage(data, '*'); | ||||
|         }; | ||||
| 
 | ||||
|         /** | ||||
|          * Send a xAPI statement to LMS. | ||||
|          * | ||||
|          * @param {string} component | ||||
|          * @param {Object} statements | ||||
|          */ | ||||
|         self.post = function(component, statements) { | ||||
|             window.parent.postMessage({ | ||||
|                 environment: 'moodleapp', | ||||
|                 context: 'h5p', | ||||
|                 action: 'xapi_post_statement', | ||||
|                 component: component, | ||||
|                 statements: statements, | ||||
|             }, '*'); | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return (window.postMessage && window.addEventListener ? new Communicator() : undefined); | ||||
| })(); | ||||
| 
 | ||||
| document.onreadystatechange = function() { | ||||
|     // Wait for instances to be initialize.
 | ||||
|     if (document.readyState !== 'complete') { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Check for H5P iFrame.
 | ||||
|     var iFrame = document.querySelector('.h5p-iframe'); | ||||
|     if (!iFrame || !iFrame.contentWindow) { | ||||
|         return; | ||||
|     } | ||||
|     var H5P = iFrame.contentWindow.H5P; | ||||
| 
 | ||||
|     // Check for H5P instances.
 | ||||
|     if (!H5P || !H5P.instances || !H5P.instances[0]) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     var resizeDelay; | ||||
|     var instance = H5P.instances[0]; | ||||
|     var parentIsFriendly = false; | ||||
| 
 | ||||
|     // Handle that the resizer is loaded after the iframe.
 | ||||
|     H5PEmbedCommunicator.on('ready', function() { | ||||
|         H5PEmbedCommunicator.send('hello'); | ||||
|     }); | ||||
| 
 | ||||
|     // Handle hello message from our parent window.
 | ||||
|     H5PEmbedCommunicator.on('hello', function() { | ||||
|         // Initial setup/handshake is done.
 | ||||
|         parentIsFriendly = true; | ||||
| 
 | ||||
|         // Hide scrollbars for correct size.
 | ||||
|         iFrame.contentDocument.body.style.overflow = 'hidden'; | ||||
| 
 | ||||
|         document.body.classList.add('h5p-resizing'); | ||||
| 
 | ||||
|         // Content need to be resized to fit the new iframe size.
 | ||||
|         H5P.trigger(instance, 'resize'); | ||||
|     }); | ||||
| 
 | ||||
|     // When resize has been prepared tell parent window to resize.
 | ||||
|     H5PEmbedCommunicator.on('resizePrepared', function() { | ||||
|         H5PEmbedCommunicator.send('resize', { | ||||
|             scrollHeight: iFrame.contentDocument.body.scrollHeight | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     H5PEmbedCommunicator.on('resize', function() { | ||||
|         H5P.trigger(instance, 'resize'); | ||||
|     }); | ||||
| 
 | ||||
|     H5P.on(instance, 'resize', function() { | ||||
|         if (H5P.isFullscreen) { | ||||
|             return; // Skip iframe resize.
 | ||||
|         } | ||||
| 
 | ||||
|         // Use a delay to make sure iframe is resized to the correct size.
 | ||||
|         clearTimeout(resizeDelay); | ||||
|         resizeDelay = setTimeout(function() { | ||||
|             // Only resize if the iframe can be resized.
 | ||||
|             if (parentIsFriendly) { | ||||
|                 H5PEmbedCommunicator.send('prepareResize', | ||||
|                     { | ||||
|                         scrollHeight: iFrame.contentDocument.body.scrollHeight, | ||||
|                         clientHeight: iFrame.contentDocument.body.clientHeight | ||||
|                     } | ||||
|                 ); | ||||
|             } else { | ||||
|                 H5PEmbedCommunicator.send('hello'); | ||||
|             } | ||||
|         }, 0); | ||||
|     }); | ||||
| 
 | ||||
|     // Get emitted xAPI data.
 | ||||
|     H5P.externalDispatcher.on('xAPI', function(event) { | ||||
|         var moodlecomponent = H5P.getMoodleComponent(); | ||||
|         if (moodlecomponent == undefined) { | ||||
|             return; | ||||
|         } | ||||
|         // Skip malformed events.
 | ||||
|         var hasStatement = event && event.data && event.data.statement; | ||||
|         if (!hasStatement) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var statement = event.data.statement; | ||||
|         var validVerb = statement.verb && statement.verb.id; | ||||
|         if (!validVerb) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' | ||||
|                     || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed'; | ||||
| 
 | ||||
|         var isChild = statement.context && statement.context.contextActivities && | ||||
|         statement.context.contextActivities.parent && | ||||
|         statement.context.contextActivities.parent[0] && | ||||
|         statement.context.contextActivities.parent[0].id; | ||||
| 
 | ||||
|         if (isCompleted && !isChild) { | ||||
|             var statements = H5P.getXAPIStatements(this.contentId, statement); | ||||
|             H5PEmbedCommunicator.post(moodlecomponent, statements); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Trigger initial resize for instance.
 | ||||
|     H5P.trigger(instance, 'resize'); | ||||
| }; | ||||
							
								
								
									
										55
									
								
								src/core/features/h5p/assets/moodle/js/h5p_overrides.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/core/features/h5p/assets/moodle/js/h5p_overrides.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| // This file is part of Moodle - http://moodle.org/
 | ||||
| //
 | ||||
| // Moodle is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| //
 | ||||
| // Moodle is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| //
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| H5P.findInstanceFromId = function (contentId) { | ||||
|     if (!contentId) { | ||||
|         return H5P.instances[0]; | ||||
|     } | ||||
|     if (H5P.instances !== undefined) { | ||||
|         for (var i = 0; i < H5P.instances.length; i++) { | ||||
|             if (H5P.instances[i].contentId === contentId) { | ||||
|                 return H5P.instances[i]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return undefined; | ||||
| }; | ||||
| H5P.getXAPIStatements = function (contentId, statement) { | ||||
|     var statements = []; | ||||
|     var instance = H5P.findInstanceFromId(contentId); | ||||
|     if (!instance){ | ||||
|         return statements; | ||||
|     } | ||||
|     if (instance.getXAPIData == undefined) { | ||||
|         var xAPIData = { | ||||
|             statement: statement | ||||
|         }; | ||||
|     } else { | ||||
|         var xAPIData = instance.getXAPIData(); | ||||
|     } | ||||
|     if (xAPIData.statement != undefined) { | ||||
|         statements.push(xAPIData.statement); | ||||
|     } | ||||
|     if (xAPIData.children != undefined) { | ||||
|         statements = statements.concat(xAPIData.children.map(a => a.statement)); | ||||
|     } | ||||
|     return statements; | ||||
| }; | ||||
| H5P.getMoodleComponent = function () { | ||||
|     if (H5PIntegration.moodleComponent) { | ||||
|         return H5PIntegration.moodleComponent; | ||||
|     } | ||||
|     return undefined; | ||||
| }; | ||||
							
								
								
									
										49
									
								
								src/core/features/h5p/assets/moodle/js/params.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/core/features/h5p/assets/moodle/js/params.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| // (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.
 | ||||
| 
 | ||||
| /** | ||||
|  * Handle params included in the URL and put them in the H5PIntegration object if it exists. | ||||
|  */ | ||||
| 
 | ||||
| if (window.H5PIntegration && window.H5PIntegration.contents && location.search) { | ||||
|     var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]]; | ||||
| 
 | ||||
|     var search = location.search.replace(/^\?/, ''); | ||||
|     var split = search.split('&'); | ||||
| 
 | ||||
|     split.forEach(function(param) { | ||||
|         var nameAndValue = param.split('='); | ||||
| 
 | ||||
|         if (nameAndValue[0] == 'displayOptions' && contentData) { | ||||
|             try { | ||||
|                 contentData.displayOptions = contentData.displayOptions || {}; | ||||
| 
 | ||||
|                 var displayOptions = JSON.parse(decodeURIComponent(nameAndValue[1])); | ||||
| 
 | ||||
|                 if (displayOptions && typeof displayOptions == 'object') { | ||||
|                     Object.assign(contentData.displayOptions, displayOptions); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.error('Error parsing display options', decodeURIComponent(nameAndValue[1])); | ||||
|             } | ||||
|         } else if (nameAndValue[0] == 'component') { | ||||
|             window.H5PIntegration.moodleComponent = nameAndValue[1]; | ||||
|             if (window.H5PIntegration.moodleComponent) { | ||||
|                 window.H5PIntegration.reportingIsEnabled = true; | ||||
|             } | ||||
|         } else if (nameAndValue[0] == 'trackingUrl' && contentData) { | ||||
|             contentData.url = nameAndValue[1]; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										358
									
								
								src/core/features/h5p/assets/styles/h5p-admin.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								src/core/features/h5p/assets/styles/h5p-admin.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,358 @@ | ||||
| /* Administration interface styling */ | ||||
| 
 | ||||
| .h5p-content { | ||||
|   border: 1px solid #DDD; | ||||
|   border-radius: 3px; | ||||
|   padding: 10px; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-table, | ||||
| .h5p-admin-table > tbody { | ||||
|   border: none; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-table tr:nth-child(odd), | ||||
| .h5p-data-view tr:nth-child(odd) { | ||||
|   background-color: #F9F9F9; | ||||
| } | ||||
| .h5p-admin-table tbody tr:hover { | ||||
|   background-color: #EEE; | ||||
| } | ||||
| .h5p-admin-table.empty { | ||||
|   padding: 1em; | ||||
|   background-color: #EEE; | ||||
|   font-size: 1.2em; | ||||
|   font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-table.libraries th:last-child, | ||||
| .h5p-admin-table.libraries td:last-child { | ||||
|   text-align: right; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-buttons-wrapper { | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-table.libraries button { | ||||
|   font-size: 2em; | ||||
|   cursor: pointer; | ||||
|   border: 1px solid #AAA; | ||||
|   border-radius: .2em; | ||||
|   background-color: #e0e0e0; | ||||
|   text-shadow: 0 0 0.5em #fff; | ||||
|   padding: 0; | ||||
|   line-height: 1em; | ||||
|   width: 1.125em; | ||||
|   height: 1.05em; | ||||
|   text-indent: -0.125em; | ||||
|   margin: 0.125em 0.125em 0 0.125em; | ||||
| } | ||||
| .h5p-admin-upgrade-library:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e888"; | ||||
| } | ||||
| .h5p-admin-view-library:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e889"; | ||||
| } | ||||
| .h5p-admin-delete-library:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e890"; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-table.libraries button:hover { | ||||
|   background-color: #d0d0d0; | ||||
| } | ||||
| .h5p-admin-table.libraries button:disabled:hover { | ||||
|   background-color: #e0e0e0; | ||||
|   cursor: default; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-upgrade-library { | ||||
|   color: #339900; | ||||
| } | ||||
| .h5p-admin-view-library { | ||||
|   color: #0066cc; | ||||
| } | ||||
| .h5p-admin-delete-library { | ||||
|   color: #990000; | ||||
| } | ||||
| .h5p-admin-delete-library:disabled, | ||||
| .h5p-admin-upgrade-library:disabled { | ||||
|   cursor: default; | ||||
|   color: #c0c0c0; | ||||
| } | ||||
| 
 | ||||
| .h5p-library-info { | ||||
|   padding: 1em 1em; | ||||
|   margin: 1em 0; | ||||
| 
 | ||||
|   width: 350px; | ||||
| 
 | ||||
|   border: 1px solid #DDD; | ||||
|   border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| /* Labeled field (label + value) */ | ||||
| .h5p-labeled-field { | ||||
|   border-bottom: 1px solid #ccc; | ||||
| } | ||||
| .h5p-labeled-field:last-child { | ||||
|   border-bottom: none; | ||||
| } | ||||
| 
 | ||||
| .h5p-labeled-field .h5p-label { | ||||
|   display: inline-block; | ||||
|   min-width: 150px; | ||||
|   font-size: 1.2em; | ||||
|   font-weight: bold; | ||||
|   padding: 0.2em; | ||||
| } | ||||
| 
 | ||||
| .h5p-labeled-field .h5p-value { | ||||
|   display: inline-block; | ||||
|   padding: 0.2em; | ||||
| } | ||||
| 
 | ||||
| /* Search element */ | ||||
| .h5p-content-search { | ||||
|   display: inline-block; | ||||
|   position: relative; | ||||
| 
 | ||||
|   width: 100%; | ||||
|   padding: 5px 0; | ||||
|   margin-top: 10px; | ||||
| 
 | ||||
|   border: 1px solid #CCC; | ||||
|   border-radius: 3px; | ||||
|   box-shadow: 2px 2px 5px #888888; | ||||
| } | ||||
| .h5p-content-search:before { | ||||
|   font-family: 'H5P'; | ||||
|   vertical-align: bottom; | ||||
|   content: "\e88a"; | ||||
|   font-size: 2em; | ||||
|   line-height: 1.25em; | ||||
| } | ||||
| .h5p-content-search input { | ||||
|   font-size: 120%; | ||||
|   line-height: 120%; | ||||
| } | ||||
| .h5p-admin-search-results { | ||||
|   margin-left: 10px; | ||||
|   color: #888; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-pager-size-selector { | ||||
|   position: absolute; | ||||
|   right: 10px; | ||||
|   top: .75em; | ||||
|   display: inline-block; | ||||
| } | ||||
| .h5p-admin-pager-size-selector > span { | ||||
|   padding: 5px; | ||||
|   margin-left: 10px; | ||||
|   cursor: pointer; | ||||
|   border: 1px solid #CCC; | ||||
|   border-radius: 3px; | ||||
| } | ||||
| .h5p-admin-pager-size-selector > span.selected { | ||||
|   background-color: #edf5fa; | ||||
| } | ||||
| .h5p-admin-pager-size-selector > span:hover { | ||||
|   background-color: #555; | ||||
|   color: #FFF; | ||||
| } | ||||
| 
 | ||||
| /* Generic "javascript"-action button */ | ||||
| button.h5p-admin { | ||||
|   border: 1px solid #AAA; | ||||
|   border-radius: 5px; | ||||
|   padding: 3px 10px; | ||||
|   background-color: #EEE; | ||||
|   cursor: pointer; | ||||
|   display: inline-block; | ||||
|   text-align: center; | ||||
|   color: #222; | ||||
| } | ||||
| button.h5p-admin:hover { | ||||
|   background-color: #555; | ||||
|   color: #FFF; | ||||
| } | ||||
| button.h5p-admin.disabled, | ||||
| button.h5p-admin.disabled:hover { | ||||
|   cursor: auto; | ||||
|   color: #CCC; | ||||
|   background-color: #FFF; | ||||
| } | ||||
| 
 | ||||
| /* Pager element */ | ||||
| .h5p-content-pager { | ||||
|   display: inline-block; | ||||
|   border: 1px solid #CCC; | ||||
|   border-radius: 3px; | ||||
|   box-shadow: 2px 2px 5px #888888; | ||||
|   width: 100%; | ||||
|   text-align: center; | ||||
|   padding: 3px 0; | ||||
| } | ||||
| .h5p-content-pager > button { | ||||
|   min-width: 80px; | ||||
|   font-size: 130%; | ||||
|   line-height: 130%; | ||||
|   border: none; | ||||
|   background: none; | ||||
|   font-family: 'H5P'; | ||||
|   font-size: 1.4em; | ||||
| } | ||||
| .h5p-content-pager > button:focus { | ||||
|   outline: 0; | ||||
| } | ||||
| .h5p-content-pager > button:last-child { | ||||
|   margin-left: 10px; | ||||
| } | ||||
| .h5p-content-pager > .pager-info { | ||||
|   cursor: pointer; | ||||
|   padding: 5px; | ||||
|   border-radius: 3px; | ||||
| } | ||||
| .h5p-content-pager > .pager-info:hover { | ||||
|   background-color: #555; | ||||
|   color: #FFF; | ||||
| } | ||||
| .h5p-content-pager > .pager-info, | ||||
| .h5p-content-pager > .h5p-pager-goto { | ||||
|   margin: 0 10px; | ||||
|   line-height: 130%; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .h5p-admin-header { | ||||
|   margin-top: 1.5em; | ||||
| } | ||||
| #h5p-library-upload-form.h5p-admin-upload-libraries-form, | ||||
| #h5p-content-type-cache-update-form.h5p-admin-upload-libraries-form { | ||||
|   position: relative; | ||||
|   margin: 0; | ||||
| 
 | ||||
| } | ||||
| .h5p-admin-upload-libraries-form .form-submit { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
| } | ||||
| .h5p-spinner { | ||||
|   padding: 0 0.5em; | ||||
|   font-size: 1.5em; | ||||
|   font-weight: bold; | ||||
| } | ||||
| #h5p-admin-container .h5p-admin-center { | ||||
|   text-align: center; | ||||
| } | ||||
| .h5p-pagination { | ||||
|   text-align: center; | ||||
| } | ||||
| .h5p-pagination > span, .h5p-pagination > input { | ||||
|   margin: 0 1em; | ||||
| } | ||||
| .h5p-data-view input[type="text"] { | ||||
|   margin-bottom: 0.5em; | ||||
|   margin-right: 0.5em; | ||||
|   float: left; | ||||
| } | ||||
| .h5p-data-view input[type="text"]::-ms-clear { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .h5p-data-view .h5p-others-contents-toggler-wrapper { | ||||
|   float: right; | ||||
|   line-height: 2; | ||||
|   margin-right: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .h5p-data-view .h5p-others-contents-toggler-label { | ||||
|   font-size: 14px; | ||||
| } | ||||
| 
 | ||||
| .h5p-data-view .h5p-others-contents-toggler { | ||||
|   margin-right: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .h5p-data-view th[role="button"] { | ||||
|   cursor: pointer; | ||||
| } | ||||
| .h5p-data-view th[role="button"].h5p-sort:after, | ||||
| .h5p-data-view th[role="button"]:hover:after, | ||||
| .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after { | ||||
|   content: "\25BE"; | ||||
|   position: relative; | ||||
|   left: 0.5em; | ||||
|   top: -1px; | ||||
| } | ||||
| .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:after, | ||||
| .h5p-data-view th[role="button"].h5p-sort:hover:after { | ||||
|   content: "\25B4"; | ||||
|   top: -2px; | ||||
| } | ||||
| .h5p-data-view th[role="button"]:hover:after, | ||||
| .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after, | ||||
| .h5p-data-view th[role="button"].h5p-sort:hover:after { | ||||
|   color: #999; | ||||
| } | ||||
| .h5p-data-view .h5p-facet { | ||||
|   cursor: pointer; | ||||
|   color: #0073aa; | ||||
|   outline: none; | ||||
| } | ||||
| .h5p-data-view .h5p-facet:hover, | ||||
| .h5p-data-view .h5p-facet:active { | ||||
|   color: #00a0d2; | ||||
| } | ||||
| .h5p-data-view .h5p-facet:focus { | ||||
|   color: #124964; | ||||
|   box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8); | ||||
| } | ||||
| .h5p-data-view .h5p-facet-wrapper { | ||||
|   line-height: 23px; | ||||
| } | ||||
| .h5p-data-view .h5p-facet-tag { | ||||
|   margin: 2px 0 0 0.5em; | ||||
|   font-size: 12px; | ||||
|   background: #e8e8e8; | ||||
|   border: 1px solid #cbcbcc; | ||||
|   border-radius: 5px; | ||||
|   color: #5d5d5d; | ||||
|   padding: 0 24px 0 10px; | ||||
|   display: inline-block; | ||||
|   position: relative; | ||||
| } | ||||
| .h5p-data-view .h5p-facet-tag > span { | ||||
|   position: absolute; | ||||
|   right: 0; | ||||
|   top: auto; | ||||
|   bottom: auto; | ||||
|   font-size: 18px; | ||||
|   color: #a2a2a2; | ||||
|   outline: none; | ||||
|   width: 21px; | ||||
|   text-indent: 4px; | ||||
|   letter-spacing: 10px; | ||||
|   overflow: hidden; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .h5p-data-view .h5p-facet-tag > span:before { | ||||
|   content: "×"; | ||||
|   font-weight: bold; | ||||
| } | ||||
| .h5p-data-view .h5p-facet-tag > span:hover, | ||||
| .h5p-data-view .h5p-facet-tag > span:focus { | ||||
|   color: #a20000; | ||||
| } | ||||
| .h5p-data-view .h5p-facet-tag > span:active { | ||||
|   color: #d20000; | ||||
| } | ||||
| .content-upgrade-log { | ||||
|   color: red; | ||||
| } | ||||
							
								
								
									
										183
									
								
								src/core/features/h5p/assets/styles/h5p-confirmation-dialog.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/core/features/h5p/assets/styles/h5p-confirmation-dialog.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | ||||
| .h5p-confirmation-dialog-background { | ||||
|   position: fixed; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   left: 0; | ||||
|   top: 0; | ||||
| 
 | ||||
|   background: rgba(44, 44, 44, 0.9); | ||||
|   opacity: 1; | ||||
|   visibility: visible; | ||||
|   -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0s; | ||||
|   transition: opacity 0.1s linear 0s, visibility 0s linear 0s; | ||||
| 
 | ||||
|   z-index: 201; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-background.hidden { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-background.hiding { | ||||
|   opacity: 0; | ||||
|   visibility: hidden; | ||||
|   -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0.1s; | ||||
|   transition: opacity 0.1s linear 0s, visibility 0s linear 0.1s; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup:focus { | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup { | ||||
|   position: absolute; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: center; | ||||
| 
 | ||||
|   box-sizing: border-box; | ||||
|   max-width: 35em; | ||||
|   min-width: 25em; | ||||
| 
 | ||||
|   top: 2em; | ||||
|   left: 50%; | ||||
|   -webkit-transform: translate(-50%, 0%); | ||||
|   -ms-transform: translate(-50%, 0%); | ||||
|   transform: translate(-50%, 0%); | ||||
| 
 | ||||
|   color: #555; | ||||
|   box-shadow: 0 0 6px 6px rgba(10,10,10,0.3); | ||||
| 
 | ||||
|   -webkit-transition: transform 0.1s ease-in; | ||||
|   transition: transform 0.1s ease-in; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup.hidden { | ||||
|   -webkit-transform: translate(-50%, 50%); | ||||
|   -ms-transform: translate(-50%, 50%); | ||||
|   transform: translate(-50%, 50%); | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-header { | ||||
|   padding: 1.5em; | ||||
|   background: #fff; | ||||
|   color: #356593; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-header-text { | ||||
|   font-size: 1.25em; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-body { | ||||
|   background: #fafbfc; | ||||
|   border-top: solid 1px #dde0e9; | ||||
|   padding: 1.25em 1.5em; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-text { | ||||
|   margin-bottom: 1.5em; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-buttons { | ||||
|   float: right; | ||||
| } | ||||
| 
 | ||||
| button.h5p-confirmation-dialog-exit:visited, | ||||
| button.h5p-confirmation-dialog-exit:link, | ||||
| button.h5p-confirmation-dialog-exit { | ||||
|   position: absolute; | ||||
|   background: none; | ||||
|   border: none; | ||||
|   font-size: 2.5em; | ||||
|   top: -0.9em; | ||||
|   right: -1.15em; | ||||
|   color: #fff; | ||||
|   cursor: pointer; | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| button.h5p-confirmation-dialog-exit:focus, | ||||
| button.h5p-confirmation-dialog-exit:hover { | ||||
|   color: #E4ECF5; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-exit:before { | ||||
|   font-family: "H5P"; | ||||
|   content: "\e890"; | ||||
| } | ||||
| 
 | ||||
| .h5p-core-button.h5p-confirmation-dialog-confirm-button { | ||||
|   padding-left: 0.75em; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .h5p-core-button.h5p-confirmation-dialog-confirm-button:before { | ||||
|   content: "\e601"; | ||||
|   margin-top: -6px; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-buttons { | ||||
|   float: none; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup.offline .count-down { | ||||
|   font-family: Arial; | ||||
|   margin-top: 0.15em; | ||||
|   color: #000; | ||||
| } | ||||
| 
 | ||||
| .h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-confirm-button:before { | ||||
|   content: "\e90b"; | ||||
|   font-weight: normal; | ||||
|   vertical-align: text-bottom; | ||||
| } | ||||
| 
 | ||||
| .throbber-wrapper { | ||||
|   display: none; | ||||
|   position: absolute; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   z-index: 1; | ||||
|   background: rgba(44, 44, 44, 0.9); | ||||
| } | ||||
| 
 | ||||
| .throbber-wrapper.show { | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .throbber-wrapper .throbber-container { | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   transform: translate(-50%, -50%); | ||||
| } | ||||
| 
 | ||||
| .throbber-wrapper .sending-requests-throbber{ | ||||
|   position: absolute; | ||||
|   top: 7em; | ||||
|   left: 50%; | ||||
|   transform: translateX(-50%); | ||||
| } | ||||
| 
 | ||||
| .throbber-wrapper .sending-requests-throbber:before { | ||||
|   display: block; | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e90b"; | ||||
|   color: white; | ||||
|   font-size: 10em; | ||||
|   animation: request-throbber 1.5s infinite linear; | ||||
| } | ||||
| 
 | ||||
| @keyframes request-throbber { | ||||
|   from { | ||||
|     transform: rotate(0); | ||||
|   } | ||||
| 
 | ||||
|   to { | ||||
|     transform: rotate(359deg); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/core/features/h5p/assets/styles/h5p-core-button.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/core/features/h5p/assets/styles/h5p-core-button.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| button.h5p-core-button:visited, | ||||
| button.h5p-core-button:link, | ||||
| button.h5p-core-button { | ||||
|   font-family: "Open Sans", sans-serif; | ||||
|   font-weight: 600; | ||||
|   font-size: 1em; | ||||
|   line-height: 1.2; | ||||
|   padding: 0.5em 1.25em; | ||||
|   border-radius: 2em; | ||||
| 
 | ||||
|   background: #2579c6; | ||||
|   color: #fff; | ||||
| 
 | ||||
|   cursor: pointer; | ||||
|   border: none; | ||||
|   box-shadow: none; | ||||
|   outline: none; | ||||
| 
 | ||||
|   display: inline-block; | ||||
|   text-align: center; | ||||
|   text-shadow: none; | ||||
|   vertical-align: baseline; | ||||
|   text-decoration: none; | ||||
| 
 | ||||
|   -webkit-transition: initial; | ||||
|   transition: initial; | ||||
| } | ||||
| button.h5p-core-button:focus { | ||||
|   background: #1f67a8; | ||||
| } | ||||
| button.h5p-core-button:hover { | ||||
|   background: rgba(31, 103, 168, 0.83); | ||||
| } | ||||
| button.h5p-core-button:active { | ||||
|   background: #104888; | ||||
| } | ||||
| button.h5p-core-button:before { | ||||
|   font-family: 'H5P'; | ||||
|   padding-right: 0.15em; | ||||
|   font-size: 1.5em; | ||||
|   vertical-align: middle; | ||||
|   line-height: 0.7; | ||||
| } | ||||
| button.h5p-core-cancel-button:visited, | ||||
| button.h5p-core-cancel-button:link, | ||||
| button.h5p-core-cancel-button { | ||||
|   border: none; | ||||
|   background: none; | ||||
|   color: #a00; | ||||
|   margin-right: 1em; | ||||
|   font-size: 1em; | ||||
|   text-decoration: none; | ||||
|   cursor: pointer; | ||||
| } | ||||
| button.h5p-core-cancel-button:hover, | ||||
| button.h5p-core-cancel-button:focus { | ||||
|   background: none; | ||||
|   border: none; | ||||
|   color: #e40000; | ||||
| } | ||||
							
								
								
									
										566
									
								
								src/core/features/h5p/assets/styles/h5p.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								src/core/features/h5p/assets/styles/h5p.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,566 @@ | ||||
| /* General CSS for H5P. Licensed under the MIT License.*/ | ||||
| 
 | ||||
| /* Custom H5P font to use for icons. */ | ||||
| @font-face { | ||||
|   font-family: 'h5p'; | ||||
|   src:    url('../fonts/h5p-core-23.eot?mz1lkp'); | ||||
|   src:    url('../fonts/h5p-core-23.eot?mz1lkp#iefix') format('embedded-opentype'), | ||||
|   url('../fonts/h5p-core-23.ttf?mz1lkp') format('truetype'), | ||||
|   url('../fonts/h5p-core-23.woff?mz1lkp') format('woff'), | ||||
|   url('../fonts/h5p-core-23.svg?mz1lkp#h5p') format('svg'); | ||||
|   font-weight: normal; | ||||
|   font-style: normal; | ||||
| } | ||||
| 
 | ||||
| html.h5p-iframe, html.h5p-iframe > body { | ||||
|   font-family: Sans-Serif; /* Use the browser's default sans-serif font. (Since Heletica doesn't look nice on Windows, and Arial on OS X.) */ | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
| .h5p-semi-fullscreen, .h5p-fullscreen, html.h5p-iframe .h5p-container { | ||||
|   overflow: hidden; | ||||
| } | ||||
| .h5p-content { | ||||
|   position: relative; | ||||
|   background: #fefefe; | ||||
|   border: 1px solid #EEE; | ||||
|   border-bottom: none; | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
| } | ||||
| .h5p-noselect | ||||
| { | ||||
|   -khtml-user-select: none; | ||||
|   -ms-user-select: none; | ||||
|   -moz-user-select: none; | ||||
|   -webkit-user-select: none; | ||||
|   user-select: none; | ||||
| } | ||||
| html.h5p-iframe .h5p-content { | ||||
|   font-size: 16px; | ||||
|   line-height: 1.5em; | ||||
|   width: 100%; | ||||
|   height: auto; | ||||
| } | ||||
| html.h5p-iframe .h5p-fullscreen .h5p-content, | ||||
| html.h5p-iframe .h5p-semi-fullscreen .h5p-content { | ||||
|   height: 100%; | ||||
| } | ||||
| .h5p-content.h5p-no-frame, | ||||
| .h5p-fullscreen .h5p-content, | ||||
| .h5p-semi-fullscreen .h5p-content { | ||||
|   border: 0; | ||||
| } | ||||
| .h5p-container { | ||||
|   position: relative; | ||||
|   z-index: 1; | ||||
| } | ||||
| .h5p-iframe-wrapper.h5p-fullscreen { | ||||
|   background-color: #000; | ||||
| } | ||||
| body.h5p-semi-fullscreen { | ||||
|   position: fixed; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| .h5p-container.h5p-semi-fullscreen { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   z-index: 101; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   background-color: #FFF; | ||||
| } | ||||
| 
 | ||||
| .h5p-content-controls { | ||||
|   margin: 0; | ||||
|   position: absolute; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   z-index: 3; | ||||
| } | ||||
| .h5p-fullscreen .h5p-content-controls { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .h5p-content-controls > a:link, .h5p-content-controls > a:visited, a.h5p-disable-fullscreen:link, a.h5p-disable-fullscreen:visited { | ||||
|   color: #e5eef6; | ||||
| } | ||||
| 
 | ||||
| .h5p-enable-fullscreen:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e88c"; | ||||
| } | ||||
| .h5p-disable-fullscreen:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e891"; | ||||
| } | ||||
| .h5p-enable-fullscreen, .h5p-disable-fullscreen { | ||||
|   cursor: pointer; | ||||
|   color: #EEE; | ||||
|   background: rgb(0,0,0); | ||||
|   background: rgba(0,0,0,0.3); | ||||
|   line-height: 0.975em; | ||||
|   font-size: 2em; | ||||
|   width: 1.125em; | ||||
|   height: 1em; | ||||
|   text-indent: 0.04em; | ||||
| } | ||||
| .h5p-disable-fullscreen { | ||||
|   line-height: 0.925em; | ||||
|   width: 1.1em; | ||||
|   height: 0.9em; | ||||
| } | ||||
| 
 | ||||
| .h5p-enable-fullscreen:focus, | ||||
| .h5p-disable-fullscreen:focus { | ||||
|   outline-style: solid; | ||||
|   outline-width: 1px; | ||||
|   outline-offset: 0.25em; | ||||
| } | ||||
| 
 | ||||
| .h5p-enable-fullscreen:hover, .h5p-disable-fullscreen:hover { | ||||
|   background: rgba(0,0,0,0.5); | ||||
| } | ||||
| .h5p-semi-fullscreen .h5p-enable-fullscreen { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| div.h5p-fullscreen { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| .h5p-iframe-wrapper { | ||||
|   width: auto; | ||||
|   height: auto; | ||||
| } | ||||
| 
 | ||||
| .h5p-fullscreen .h5p-iframe-wrapper, | ||||
| .h5p-semi-fullscreen .h5p-iframe-wrapper { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .h5p-iframe-wrapper.h5p-semi-fullscreen { | ||||
|   width: auto; | ||||
|   height: auto; | ||||
|   background: black; | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   z-index: 100001; | ||||
| } | ||||
| .h5p-iframe-wrapper.h5p-semi-fullscreen .buttons { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   z-index: 20; | ||||
| } | ||||
| .h5p-iframe-wrapper iframe.h5p-iframe { | ||||
|   /* Hack for IOS landscape / portrait */ | ||||
|   width: 10px; | ||||
|   min-width: 100%; | ||||
|   *width: 100%; | ||||
|   /* End of hack */ | ||||
|   height: 100%; | ||||
|   z-index: 10; | ||||
|   overflow: hidden; | ||||
|   border: 0; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .h5p-content ul.h5p-actions { | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
|   overflow: hidden; | ||||
|   list-style: none; | ||||
|   padding: 0px 10px; | ||||
|   margin: 0; | ||||
|   height: 25px; | ||||
|   font-size: 12px; | ||||
|   background: #FAFAFA; | ||||
|   border-top: 1px solid #EEE; | ||||
|   border-bottom: 1px solid #EEE; | ||||
|   clear: both; | ||||
|   font-family: Sans-Serif; | ||||
| } | ||||
| .h5p-fullscreen .h5p-actions, .h5p-semi-fullscreen .h5p-actions { | ||||
|   display: none; | ||||
| } | ||||
| .h5p-actions > .h5p-button { | ||||
|   float: left; | ||||
|   cursor: pointer; | ||||
|   margin: 0 0.5em 0 0; | ||||
|   background: none; | ||||
|   padding: 0 0.75em 0 0.25em; | ||||
|   vertical-align: top; | ||||
|   color: #999; | ||||
|   text-decoration: none; | ||||
|   outline: none; | ||||
|   line-height: 23px; | ||||
| } | ||||
| .h5p-actions > .h5p-button:hover { | ||||
|   color: #666; | ||||
| } | ||||
| .h5p-actions > .h5p-button:active, | ||||
| .h5p-actions > .h5p-button:focus, | ||||
| .h5p-actions .h5p-link:active, | ||||
| .h5p-actions .h5p-link:focus { | ||||
|   color: #666; | ||||
| } | ||||
| .h5p-actions > .h5p-button:focus, | ||||
| .h5p-actions .h5p-link:focus { | ||||
|   outline-style: solid; | ||||
|   outline-width: thin; | ||||
|   outline-offset: -2px; | ||||
|   outline-color: #9ecaed; | ||||
| } | ||||
| .h5p-actions > .h5p-button:before { | ||||
|   font-family: 'H5P'; | ||||
|   font-size: 20px; | ||||
|   line-height: 20px; | ||||
|   vertical-align: top; | ||||
|   padding-right: 0; | ||||
| } | ||||
| .h5p-actions > .h5p-button.h5p-export:before { | ||||
|   content: "\e90b"; | ||||
| } | ||||
| .h5p-actions > .h5p-button.h5p-copyrights:before { | ||||
|   content: "\e88f"; | ||||
| } | ||||
| .h5p-actions > .h5p-button.h5p-embed:before { | ||||
|   content: "\e892"; | ||||
| } | ||||
| .h5p-actions .h5p-link { | ||||
|   float: right; | ||||
|   margin-right: 0; | ||||
|   font-size: 2.0em; | ||||
|   line-height: 23px; | ||||
|   overflow: hidden; | ||||
|   color: #999; | ||||
|   text-decoration: none; | ||||
|   outline: none; | ||||
| } | ||||
| .h5p-actions .h5p-link:before { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e88e"; | ||||
|   vertical-align: bottom; | ||||
| } | ||||
| .h5p-actions > li { | ||||
|   margin: 0; | ||||
|   list-style: none; | ||||
| } | ||||
| .h5p-popup-dialog { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   min-height: 100%; | ||||
|   z-index: 100; | ||||
|   padding: 2em; | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
|   opacity: 0; | ||||
|   -webkit-transition: opacity 0.2s; | ||||
|   -moz-transition: opacity 0.2s; | ||||
|   -o-transition: opacity 0.2s; | ||||
|   transition: opacity 0.2s; | ||||
|   background:#000; | ||||
|   background:rgba(0,0,0,0.75); | ||||
| } | ||||
| .h5p-popup-dialog.h5p-open { | ||||
|   opacity: 1; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-inner { | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
|   background: #fff; | ||||
|   height: 100%; | ||||
|   max-height: 100%; | ||||
|   position: relative; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-inner > h2 { | ||||
|   position: absolute; | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
|   width: 100%; | ||||
|   margin: 0; | ||||
|   background: #eee; | ||||
|   display: block; | ||||
|   color: #656565; | ||||
|   font-size: 1.25em; | ||||
|   padding: 0.325em 0.5em 0.25em; | ||||
|   line-height: 1.25em; | ||||
|   border-bottom: 1px solid #ccc; | ||||
|   z-index: 2; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-inner > h2 > a { | ||||
|   font-size: 12px; | ||||
|   margin-left: 1em; | ||||
| } | ||||
| .h5p-embed-dialog .h5p-inner, | ||||
| .h5p-reuse-dialog .h5p-inner, | ||||
| .h5p-content-user-data-reset-dialog .h5p-inner { | ||||
|   min-width: 316px; | ||||
|   max-width: 400px; | ||||
|   left: 50%; | ||||
|   top: 50%; | ||||
|   transform: translateX(-50%); | ||||
| } | ||||
| .h5p-embed-dialog .h5p-embed-code-container, | ||||
| .h5p-embed-size { | ||||
|   resize: none; | ||||
|   outline: none; | ||||
|   width: 100%; | ||||
|   padding: 0.375em 0.5em 0.25em; | ||||
|   margin: 0; | ||||
|   overflow: hidden; | ||||
|   border: 1px solid #ccc; | ||||
|   box-shadow: 0 1px 2px 0 #d0d0d0 inset; | ||||
|   font-size: 0.875em; | ||||
|   letter-spacing: 0.065em; | ||||
|   font-family: sans-serif; | ||||
|   white-space: pre; | ||||
|   line-height: 1.5em; | ||||
|   height: 2.0714em; | ||||
|   background: #f5f5f5; | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
| } | ||||
| .h5p-embed-dialog .h5p-embed-code-container:focus { | ||||
|   height: 5em; | ||||
| } | ||||
| .h5p-embed-size { | ||||
|   width: 3.5em; | ||||
|   text-align: right; | ||||
|   margin: 0.5em 0; | ||||
|   line-height: 2em; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-scroll-content { | ||||
|   border-top: 2.25em solid transparent; | ||||
|   padding: 1em; | ||||
|   box-sizing: border-box; | ||||
|   -moz-box-sizing: border-box; | ||||
|   color: #555555; | ||||
|   z-index: 1; | ||||
| } | ||||
| .h5p-popup-dialog.h5p-open .h5p-scroll-content { | ||||
|   overflow: auto; | ||||
|   overflow-x: hidden; | ||||
|   overflow-y: auto; | ||||
|   height: 100%; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar { | ||||
|   width: 8px; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-track { | ||||
|   background: #e0e0e0; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-thumb { | ||||
|   box-shadow: 0 0 10px #000 inset; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-close { | ||||
|   cursor: pointer; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-close:after { | ||||
|   font-family: 'H5P'; | ||||
|   content: "\e894"; | ||||
|   font-size: 2em; | ||||
|   position: absolute; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   width: 1.125em; | ||||
|   height: 1.125em; | ||||
|   line-height: 1.125em; | ||||
|   color: #656565; | ||||
|   cursor: pointer; | ||||
|   text-indent: -0.065em; | ||||
|   z-index: 3 | ||||
| } | ||||
| .h5p-popup-dialog .h5p-close:hover:after, | ||||
| .h5p-popup-dialog .h5p-close:focus:after { | ||||
|   color: #454545; | ||||
| } | ||||
| .h5p-popup-dialog .h5p-close:active:after { | ||||
|   color: #252525; | ||||
| } | ||||
| .h5p-poopup-dialog h2 { | ||||
|   margin: 0.25em 0 0.5em; | ||||
| } | ||||
| .h5p-popup-dialog h3 { | ||||
|   margin: 0.75em 0 0.25em; | ||||
| } | ||||
| .h5p-popup-dialog dl { | ||||
|   margin: 0.25em 0 0.75em; | ||||
| } | ||||
| .h5p-popup-dialog dt { | ||||
|   float: left; | ||||
|   margin: 0 0.75em 0 0; | ||||
| } | ||||
| .h5p-popup-dialog dt:after { | ||||
|   content: ':'; | ||||
| } | ||||
| .h5p-popup-dialog dd { | ||||
|   margin: 0; | ||||
| } | ||||
| .h5p-expander { | ||||
|   cursor: pointer; | ||||
|   font-size: 1.125em; | ||||
|   outline: none; | ||||
|   margin: 0.5em 0 0; | ||||
|   display: inline-block; | ||||
| } | ||||
| .h5p-expander:before { | ||||
|   content: "+"; | ||||
|   width: 1em; | ||||
|   display: inline-block; | ||||
|   font-weight: bold; | ||||
| } | ||||
| .h5p-expander.h5p-open:before { | ||||
|   content: "-"; | ||||
|   text-indent: 0.125em; | ||||
| } | ||||
| .h5p-expander:hover, | ||||
| .h5p-expander:focus { | ||||
|   color: #303030; | ||||
| } | ||||
| .h5p-expander:active { | ||||
|   color: #202020; | ||||
| } | ||||
| .h5p-expander-content { | ||||
|   display: none; | ||||
| } | ||||
| .h5p-expander-content p { | ||||
|   margin: 0.5em 0; | ||||
| } | ||||
| .h5p-content-copyrights { | ||||
|   border-left: 0.25em solid #d0d0d0; | ||||
|   margin-left: 0.25em; | ||||
|   padding-left: 0.25em; | ||||
| } | ||||
| .h5p-throbber { | ||||
|   background: url('../images/throbber.gif?ver=1.2.1') 10px center no-repeat; | ||||
|   padding-left: 38px; | ||||
|   min-height: 30px; | ||||
|   line-height: 30px; | ||||
| } | ||||
| .h5p-dialog-ok-button { | ||||
|   cursor: default; | ||||
|   float: right; | ||||
|   outline: none; | ||||
|   border: 2px solid #ccc; | ||||
|   padding: 0.25em 0.75em 0.125em; | ||||
|   background: #eee; | ||||
| } | ||||
| .h5p-dialog-ok-button:hover, | ||||
| .h5p-dialog-ok-button:focus { | ||||
|   background: #fafafa; | ||||
| } | ||||
| .h5p-dialog-ok-button:active { | ||||
|   background: #eeffee; | ||||
| } | ||||
| .h5p-big-button { | ||||
|   line-height: 1.25; | ||||
|   display: block; | ||||
|   position: relative; | ||||
|   cursor: pointer; | ||||
|   width: 100%; | ||||
|   padding: 1em 1em 1em 3.75em; | ||||
|   text-align: left; | ||||
|   border: 1px solid #dedede; | ||||
|   background: linear-gradient(#ffffff, #f1f1f2); | ||||
|   border-radius: 0.25em; | ||||
| } | ||||
| .h5p-big-button:before { | ||||
|   font-family: 'h5p'; | ||||
|   content: "\e893"; | ||||
|   line-height: 1; | ||||
|   font-size: 3em; | ||||
|   color: #2747f7; | ||||
|   position: absolute; | ||||
|   left: 0.125em; | ||||
|   top: 0.125em; | ||||
| } | ||||
| .h5p-copy-button:before { | ||||
|   content: "\e905"; | ||||
| } | ||||
| .h5p-big-button:hover { | ||||
|   border: 1px solid #2747f7; | ||||
|   background: #eff1fe; | ||||
| } | ||||
| .h5p-big-button:active { | ||||
|   border: 1px solid #dedede; | ||||
|   background: #dfe4fe; | ||||
| } | ||||
| .h5p-button-title { | ||||
|   color: #2747f7; | ||||
|   font-size: 15px; | ||||
|   font-weight: bold; | ||||
|   margin-bottom: 0.5em; | ||||
| } | ||||
| .h5p-button-description { | ||||
|   color: #757575; | ||||
| } | ||||
| .h5p-horizontal-line-text { | ||||
|   border-top: 1px solid #dadada; | ||||
|   line-height: 1; | ||||
|   color: #474747; | ||||
|   text-align: center; | ||||
|   position: relative; | ||||
|   margin: 1.25em 0; | ||||
| } | ||||
| .h5p-horizontal-line-text > span { | ||||
|   background: white; | ||||
|   padding: 0.5em; | ||||
|   position: absolute; | ||||
|   top: -1em; | ||||
|   left: 50%; | ||||
|   transform: translateX(-50%); | ||||
| } | ||||
| .h5p-toast { | ||||
|   font-size: 0.75em; | ||||
|   background-color: rgba(0, 0, 0, 0.9); | ||||
|   color: #fff; | ||||
|   z-index: 110; | ||||
|   position: absolute; | ||||
|   padding: 0 0.5em; | ||||
|   line-height: 2; | ||||
|   border-radius: 4px; | ||||
|   white-space: nowrap; | ||||
|   pointer-events: none; | ||||
|   top: 0; | ||||
|   opacity: 1; | ||||
|   visibility: visible; | ||||
|   transition: opacity 1s; | ||||
| } | ||||
| .h5p-toast-disabled { | ||||
|   opacity: 0; | ||||
|   visibility: hidden; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* This is loaded as part of Core and not Editor since this needs to be outside the editor iframe */ | ||||
| .h5peditor-semi-fullscreen { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   z-index: 101; | ||||
| } | ||||
| iframe.h5peditor-semi-fullscreen { | ||||
|   background: #fff; | ||||
|   z-index: 100001; | ||||
| } | ||||
| 
 | ||||
| .h5p-content.using-mouse *:not(textarea):focus { | ||||
|   outline: none !important; | ||||
| } | ||||
							
								
								
									
										1392
									
								
								src/core/features/h5p/classes/content-validator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1392
									
								
								src/core/features/h5p/classes/content-validator.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1005
									
								
								src/core/features/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1005
									
								
								src/core/features/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										475
									
								
								src/core/features/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								src/core/features/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,475 @@ | ||||
| // (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 { CoreFile } from '@services/file'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { | ||||
|     CoreH5PCore, | ||||
|     CoreH5PDependencyAsset, | ||||
|     CoreH5PContentDependencyData, | ||||
|     CoreH5PDependenciesFiles, | ||||
|     CoreH5PLibraryBasicData, | ||||
|     CoreH5PContentMainLibraryData, | ||||
| } from './core'; | ||||
| import { CONTENTS_LIBRARIES_TABLE_NAME, CONTENT_TABLE_NAME, CoreH5PLibraryCachedAssetsDBRecord } from '../services/database/h5p'; | ||||
| import { CoreH5PLibraryBeingSaved } from './storage'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFileStorage. | ||||
|  */ | ||||
| export class CoreH5PFileStorage { | ||||
| 
 | ||||
|     static readonly CACHED_ASSETS_FOLDER_NAME = 'cachedassets'; | ||||
| 
 | ||||
|     /** | ||||
|      * Will concatenate all JavaScrips and Stylesheets into two files in order to improve page performance. | ||||
|      * | ||||
|      * @param files A set of all the assets required for content to display. | ||||
|      * @param key Hashed key for cached asset. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async cacheAssets(files: CoreH5PDependenciesFiles, key: string, folderName: string, siteId: string): Promise<void> { | ||||
| 
 | ||||
|         const cachedAssetsPath = this.getCachedAssetsFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Treat each type in the assets.
 | ||||
|         await Promise.all(Object.keys(files).map(async (type) => { | ||||
| 
 | ||||
|             const assets: CoreH5PDependencyAsset[] = files[type]; | ||||
| 
 | ||||
|             if (!assets || !assets.length) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Create new file for cached assets.
 | ||||
|             const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css'); | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsPath, fileName); | ||||
| 
 | ||||
|             // Store concatenated content.
 | ||||
|             const content = await this.concatenateFiles(assets, type); | ||||
| 
 | ||||
|             await CoreFile.instance.writeFile(path, content); | ||||
| 
 | ||||
|             // Now update the files data.
 | ||||
|             files[type] = [ | ||||
|                 { | ||||
|                     path: CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, fileName), | ||||
|                     version: '', | ||||
|                 }, | ||||
|             ]; | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds all files of a type into one file. | ||||
|      * | ||||
|      * @param assets A list of files. | ||||
|      * @param type The type of files in assets. Either 'scripts' or 'styles' | ||||
|      * @return Promise resolved with all of the files content in one string. | ||||
|      */ | ||||
|     protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> { | ||||
|         const basePath = CoreFile.instance.convertFileSrc(CoreFile.instance.getBasePathInstant()); | ||||
|         let content = ''; | ||||
| 
 | ||||
|         for (const i in assets) { | ||||
|             const asset = assets[i]; | ||||
| 
 | ||||
|             let fileContent = await CoreFile.instance.readFile(asset.path); | ||||
| 
 | ||||
|             if (type == 'scripts') { | ||||
|                 // No need to treat scripts, just append the content.
 | ||||
|                 content += fileContent + ';\n'; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // Rewrite relative URLs used inside stylesheets.
 | ||||
|             const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig); | ||||
|             const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
 | ||||
|             const treated = {}; | ||||
| 
 | ||||
|             if (matches && matches.length) { | ||||
|                 matches.forEach((match) => { | ||||
|                     let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, ''); | ||||
| 
 | ||||
|                     if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) { | ||||
|                         return; // Not relative or already treated, skip.
 | ||||
|                     } | ||||
| 
 | ||||
|                     const pathSplit = assetPath.split('/'); | ||||
|                     treated[url] = url; | ||||
| 
 | ||||
|                     /* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the | ||||
|                         filepath for the first folder in the url. */ | ||||
|                     if (url.match(/^\.\.\//)) { | ||||
|                         // Split and remove empty values.
 | ||||
|                         const urlSplit = url.split('/').filter((i) => i); | ||||
| 
 | ||||
|                         // Remove the file name from the asset path.
 | ||||
|                         pathSplit.pop(); | ||||
| 
 | ||||
|                         // Remove the first element from the file URL: ../ .
 | ||||
|                         urlSplit.shift(); | ||||
| 
 | ||||
|                         // Put the url's first folder into the asset path.
 | ||||
|                         pathSplit[pathSplit.length - 1] = urlSplit[0]; | ||||
|                         urlSplit.shift(); | ||||
| 
 | ||||
|                         // Create the new URL and replace it in the file contents.
 | ||||
|                         url = pathSplit.join('/') + '/' + urlSplit.join('/'); | ||||
| 
 | ||||
|                     } else { | ||||
|                         pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
 | ||||
|                         url = pathSplit.join('/'); | ||||
|                     } | ||||
| 
 | ||||
|                     fileContent = fileContent.replace( | ||||
|                         new RegExp(CoreTextUtils.instance.escapeForRegex(match), 'g'), | ||||
|                         'url("' + CoreTextUtils.instance.concatenatePaths(basePath, url) + '")', | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             content += fileContent + '\n'; | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from file system. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteCachedAssets(removedEntries: CoreH5PLibraryCachedAssetsDBRecord[], siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         removedEntries.forEach((entry) => { | ||||
|             const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId()); | ||||
| 
 | ||||
|             ['js', 'css'].forEach((type) => { | ||||
|                 const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type); | ||||
| 
 | ||||
|                 promises.push(CoreFile.instance.removeFile(path)); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         // Ignore errors, maybe there's no cached asset of some type.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(promises)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a content folder from the file system. | ||||
|      * | ||||
|      * @param folderName Folder name of the content. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentFolder(folderName: string, siteId: string): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getContentFolderPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndex(folderName: string, siteId: string): Promise<void> { | ||||
|         await CoreFile.instance.removeFile(this.getContentIndexPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Get the folder names of all the packages that use this library.
 | ||||
|         const query = 'SELECT DISTINCT hc.foldername ' + | ||||
|                     'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' + | ||||
|                     'JOIN ' + CONTENT_TABLE_NAME + ' hc ON hcl.h5pid = hc.id ' + | ||||
|                     'WHERE hcl.libraryid = ?'; | ||||
|         const queryArgs = [libraryId]; | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         await Array.from(result.rows).map(async (entry: {foldername: string}) => { | ||||
|             try { | ||||
|                 // Delete the index.html.
 | ||||
|                 await this.deleteContentIndex(entry.foldername, site.getId()); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a library from the file system. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param siteId Site ID. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryFolder( | ||||
|         libraryData: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData, | ||||
|         siteId: string, | ||||
|         folderName?: string, | ||||
|     ): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Will check if there are cache assets available for content. | ||||
|      * | ||||
|      * @param key Hashed key for cached asset | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getCachedAssets(key: string): Promise<{scripts?: CoreH5PDependencyAsset[]; styles?: CoreH5PDependencyAsset[]} | null> { | ||||
| 
 | ||||
|         // Get JS and CSS cached assets if they exist.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.getCachedAsset(key, '.js'), | ||||
|             this.getCachedAsset(key, '.css'), | ||||
|         ]); | ||||
| 
 | ||||
|         const files = { | ||||
|             scripts: results[0], | ||||
|             styles: results[1], | ||||
|         }; | ||||
| 
 | ||||
|         return files.scripts || files.styles ? files : null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a cached asset file exists and, if so, return its data. | ||||
|      * | ||||
|      * @param key Key of the cached asset. | ||||
|      * @param extension Extension of the file to get. | ||||
|      * @return Promise resolved with the list of assets (only one), undefined if not found. | ||||
|      */ | ||||
|     protected async getCachedAsset(key: string, extension: string): Promise<CoreH5PDependencyAsset[] | undefined> { | ||||
| 
 | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, key + extension); | ||||
| 
 | ||||
|             const size = await CoreFile.instance.getFileSize(path); | ||||
| 
 | ||||
|             if (size > 0) { | ||||
|                 return [ | ||||
|                     { | ||||
|                         path: path, | ||||
|                         version: '', | ||||
|                     }, | ||||
|                 ]; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Not found, nothing to do.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get relative path to a content cached assets. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the content the assets belong to. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Path. | ||||
|      */ | ||||
|     getCachedAssetsFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|             this.getContentFolderPath(folderName, siteId), | ||||
|             CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a content folder name given the package URL. | ||||
|      * | ||||
|      * @param fileUrl Package URL. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved with the folder name. | ||||
|      */ | ||||
|     async getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise<string> { | ||||
|         const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl); | ||||
| 
 | ||||
|         const fileAndDir = CoreFile.instance.getFileAndDirectoryFromPath(path); | ||||
| 
 | ||||
|         return CoreMimetypeUtils.instance.removeExtension(fileAndDir.name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a package content path. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|             this.getExternalH5PFolderPath(siteId), | ||||
|             'packages/' + folderName + '/content', | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl(fileUrl: string, siteId?: string): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const folderName = await this.getContentFolderNameByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         const file = await CoreFile.instance.getFile(this.getContentIndexPath(folderName, siteId)); | ||||
| 
 | ||||
|         return file.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to a content index. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentIndexPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder that contains the H5P core libraries. | ||||
|      * | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getCoreH5PPath(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getWWWPath(), '/h5p/'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the dependency. | ||||
|      * | ||||
|      * @param dependency Dependency library. | ||||
|      * @return The path to the dependency library | ||||
|      */ | ||||
|     getDependencyPath(dependency: CoreH5PContentDependencyData): string { | ||||
|         return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get path to the folder containing H5P files extracted from packages. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getExternalH5PFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getSiteFolder(siteId), 'h5p'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get libraries folder path. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibrariesFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'libraries'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library's folder path. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param siteId The site ID. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibraryFolderPath( | ||||
|         libraryData: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData, | ||||
|         siteId: string, | ||||
|         folderName?: string, | ||||
|     ): string { | ||||
|         if (!folderName) { | ||||
|             folderName = CoreH5PCore.libraryToString(libraryData, true); | ||||
|         } | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the content in filesystem. | ||||
|      * | ||||
|      * @param contentPath Path to the current content folder (tmp). | ||||
|      * @param folderName Name to put to the content folder. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveContent(contentPath: string, folderName: string, siteId: string): Promise<void> { | ||||
|         const folderPath = this.getContentFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Delete existing content for this package.
 | ||||
|         await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath)); | ||||
| 
 | ||||
|         // Copy the new one.
 | ||||
|         await CoreFile.instance.moveDir(contentPath, folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a library in filesystem. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibrary(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const folderPath = this.getLibraryFolderPath(libraryData, siteId); | ||||
| 
 | ||||
|         // Delete existing library version.
 | ||||
|         try { | ||||
|             await CoreFile.instance.removeDir(folderPath); | ||||
|         } catch (error) { | ||||
|             // Ignore errors, maybe it doesn't exist.
 | ||||
|         } | ||||
| 
 | ||||
|         if (libraryData.uploadDirectory) { | ||||
|             // Copy the new one.
 | ||||
|             await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										922
									
								
								src/core/features/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										922
									
								
								src/core/features/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,922 @@ | ||||
| // (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 { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreH5P } from '@features/h5p/services/h5p'; | ||||
| import { | ||||
|     CoreH5PCore, | ||||
|     CoreH5PDisplayOptionBehaviour, | ||||
|     CoreH5PContentDependencyData, | ||||
|     CoreH5PLibraryData, | ||||
|     CoreH5PLibraryAddonData, | ||||
|     CoreH5PContentDepsTreeDependency, | ||||
|     CoreH5PLibraryBasicData, | ||||
|     CoreH5PLibraryBasicDataWithPatch, | ||||
| } from './core'; | ||||
| import { | ||||
|     CONTENT_TABLE_NAME, | ||||
|     LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|     CoreH5PLibraryCachedAssetsDBRecord, | ||||
|     LIBRARIES_TABLE_NAME, | ||||
|     LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|     CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|     CoreH5PContentDBRecord, | ||||
|     CoreH5PLibraryDBRecord, | ||||
|     CoreH5PLibraryDependencyDBRecord, | ||||
|     CoreH5PContentsLibraryDBRecord, | ||||
| } from '../services/database/h5p'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreH5PSemantics } from './content-validator'; | ||||
| import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage'; | ||||
| import { CoreH5PLibraryAddTo, CoreH5PLibraryMetadataSettings } from './validator'; | ||||
| import { CoreH5PMetadata } from './metadata'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFrameworkInterface. | ||||
|  */ | ||||
| export class CoreH5PFramework { | ||||
| 
 | ||||
|     /** | ||||
|      * Will clear filtered params for all the content that uses the specified libraries. | ||||
|      * This means that the content dependencies will have to be rebuilt and the parameters re-filtered. | ||||
|      * | ||||
|      * @param libraryIds Array of library ids. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async clearFilteredParameters(libraryIds: number[], siteId?: string): Promise<void> { | ||||
|         if (!libraryIds || !libraryIds.length) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const whereAndParams = db.getInOrEqual(libraryIds); | ||||
|         whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0]; | ||||
| 
 | ||||
|         await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams[0], whereAndParams[1]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from DB. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the removed entries. | ||||
|      */ | ||||
|     async deleteCachedAssets(libraryId: number, siteId?: string): Promise<CoreH5PLibraryCachedAssetsDBRecord[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Get all the hashes that use this library.
 | ||||
|         const entries = await db.getRecords<CoreH5PLibraryCachedAssetsDBRecord>( | ||||
|             LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             { libraryid: libraryId }, | ||||
|         ); | ||||
| 
 | ||||
|         const hashes = entries.map((entry) => entry.hash); | ||||
| 
 | ||||
|         if (hashes.length) { | ||||
|             // Delete the entries from DB.
 | ||||
|             await db.deleteRecordsList(LIBRARIES_CACHEDASSETS_TABLE_NAME, 'hash', hashes); | ||||
|         } | ||||
| 
 | ||||
|         return entries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentData(id: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             // Delete the content data.
 | ||||
|             db.deleteRecords(CONTENT_TABLE_NAME, { id }), | ||||
| 
 | ||||
|             // Remove content library dependencies.
 | ||||
|             this.deleteLibraryUsage(id, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete library data from DB. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibrary(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(LIBRARIES_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all dependencies belonging to given library. | ||||
|      * | ||||
|      * @param libraryId Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(LIBRARY_DEPENDENCIES_TABLE_NAME, { libraryid: libraryId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete what libraries a content item is using. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryUsage(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(CONTENTS_LIBRARIES_TABLE_NAME, { h5pid: id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all conent data from DB. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the list of content data. | ||||
|      */ | ||||
|     async getAllContentData(siteId?: string): Promise<CoreH5PContentDBRecord[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getAllRecords<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentData(id: number, siteId?: string): Promise<CoreH5PContentDBRecord> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param fileUrl H5P file URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentDataByUrl(fileUrl: string, siteId?: string): Promise<CoreH5PContentDBRecord> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Try to use the folder name, it should be more reliable than the URL.
 | ||||
|         const folderName = await CoreH5P.instance.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, site.getId()); | ||||
| 
 | ||||
|         try { | ||||
|             return await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { foldername: folderName }); | ||||
|         } catch (error) { | ||||
|             // Cannot get folder name, the h5p file was probably deleted. Just use the URL.
 | ||||
|             return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { fileurl: fileUrl }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the latest library version. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @return Promise resolved with the latest library version data. | ||||
|      */ | ||||
|     async getLatestLibraryVersion(machineName: string, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         try { | ||||
|             const records = await db.getRecords<CoreH5PLibraryDBRecord>( | ||||
|                 LIBRARIES_TABLE_NAME, | ||||
|                 { machinename: machineName }, | ||||
|                 'majorversion DESC, minorversion DESC, patchversion DESC', | ||||
|                 '*', | ||||
|                 0, | ||||
|                 1, | ||||
|             ); | ||||
| 
 | ||||
|             if (records && records[0]) { | ||||
|                 return this.parseLibDBData(records[0]); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Library not found.
 | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError(`Missing required library: ${machineName}`); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     protected async getLibrary( | ||||
|         machineName: string, | ||||
|         majorVersion?: string | number, | ||||
|         minorVersion?: string | number, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const libraries = await db.getRecords<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, { | ||||
|             machinename: machineName, | ||||
|             majorversion: majorVersion, | ||||
|             minorversion: minorVersion, | ||||
|         }); | ||||
| 
 | ||||
|         if (!libraries.length) { | ||||
|             throw new CoreError('Libary not found.'); | ||||
|         } | ||||
| 
 | ||||
|         return this.parseLibDBData(libraries[0]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     getLibraryByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
|         return this.getLibrary(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB by ID. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     async getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const library = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, { id }); | ||||
| 
 | ||||
|         return this.parseLibDBData(library); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     async getLibraryId( | ||||
|         machineName: string, | ||||
|         majorVersion?: string | number, | ||||
|         minorVersion?: string | number, | ||||
|         siteId?: string, | ||||
|     ): Promise<number | undefined> { | ||||
|         try { | ||||
|             const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|             return library.id || undefined; | ||||
|         } catch (error) { | ||||
|             return undefined; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     getLibraryIdByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<number | undefined> { | ||||
|         return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the default behaviour for the display option defined. | ||||
|      * | ||||
|      * @param name Identifier for the setting. | ||||
|      * @param defaultValue Optional default value if settings is not set. | ||||
|      * @return Return the value for this display option. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     getOption(name: string, defaultValue: unknown): unknown { | ||||
|         // For now, all them are disabled by default, so only will be rendered when defined in the display options.
 | ||||
|         return CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_AUTHOR_DEFAULT_OFF; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the user has permission to execute an action. | ||||
|      * | ||||
|      * @param permission Permission to check. | ||||
|      * @param id H5P package id. | ||||
|      * @return Whether the user has permission to execute an action. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     hasPermission(permission: number, id: number): boolean { | ||||
|         // H5P capabilities have not been introduced.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determines if content slug is used. | ||||
|      * | ||||
|      * @param slug The content slug. | ||||
|      * @return Whether the content slug is used | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     isContentSlugAvailable(slug: string): boolean { | ||||
|         // By default the slug should be available as it's currently generated as a unique value for each h5p content.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a library is a patched version of the one installed. | ||||
|      * | ||||
|      * @param library Library to check. | ||||
|      * @param dbData Installed library. If not supplied it will be calculated. | ||||
|      * @return Promise resolved with boolean: whether it's a patched library. | ||||
|      */ | ||||
|     async isPatchedLibrary(library: CoreH5PLibraryBasicDataWithPatch, dbData?: CoreH5PLibraryParsedDBRecord): Promise<boolean> { | ||||
|         if (!dbData) { | ||||
|             dbData = await this.getLibraryByData(library); | ||||
|         } | ||||
| 
 | ||||
|         return library.patchVersion > dbData.patchversion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert list of library parameter values to csv. | ||||
|      * | ||||
|      * @param libraryData Library data as found in library.json files. | ||||
|      * @param key Key that should be found in libraryData. | ||||
|      * @param searchParam The library parameter (Default: 'path'). | ||||
|      * @return Library parameter values separated by ', ' | ||||
|      */ | ||||
|     libraryParameterValuesToCsv(libraryData: CoreH5PLibraryBeingSaved, key: string, searchParam: string = 'path'): string { | ||||
|         if (typeof libraryData[key] != 'undefined') { | ||||
|             const parameterValues: string[] = []; | ||||
| 
 | ||||
|             libraryData[key].forEach((file) => { | ||||
|                 for (const index in file) { | ||||
|                     if (index === searchParam) { | ||||
|                         parameterValues.push(file[index]); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return parameterValues.join(','); | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load addon libraries. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the addon libraries. | ||||
|      */ | ||||
|     async loadAddons(siteId?: string): Promise<CoreH5PLibraryAddonData[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const query = 'SELECT l1.id AS libraryId, l1.machinename AS machineName, ' + | ||||
|                         'l1.majorversion AS majorVersion, l1.minorversion AS minorVersion, ' + | ||||
|                         'l1.patchversion AS patchVersion, l1.addto AS addTo, ' + | ||||
|                         'l1.preloadedjs AS preloadedJs, l1.preloadedcss AS preloadedCss ' + | ||||
|                     'FROM ' + LIBRARIES_TABLE_NAME + ' l1 ' + | ||||
|                     'JOIN ' + LIBRARIES_TABLE_NAME + ' l2 ON l1.machinename = l2.machinename AND (' + | ||||
|                         'l1.majorversion < l2.majorversion OR (l1.majorversion = l2.majorversion AND ' + | ||||
|                         'l1.minorversion < l2.minorversion)) ' + | ||||
|                     'WHERE l1.addto IS NOT NULL AND l2.machinename IS NULL'; | ||||
| 
 | ||||
|         const result = await db.execute(query); | ||||
| 
 | ||||
|         const addons: CoreH5PLibraryAddonData[] = []; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             addons.push(this.parseLibAddonData(result.rows.item(i))); | ||||
|         } | ||||
| 
 | ||||
|         return addons; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param fileUrl H5P file URL. Required if id is not provided. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async loadContent(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PFrameworkContentData> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         let contentData: CoreH5PContentDBRecord; | ||||
| 
 | ||||
|         if (id) { | ||||
|             contentData = await this.getContentData(id, siteId); | ||||
|         } else if (fileUrl) { | ||||
|             contentData = await this.getContentDataByUrl(fileUrl, siteId); | ||||
|         } else { | ||||
|             throw new CoreError('No id or fileUrl supplied to loadContent.'); | ||||
|         } | ||||
| 
 | ||||
|         // Load the main library data.
 | ||||
|         const libData = await this.getLibraryById(contentData.mainlibraryid, siteId); | ||||
| 
 | ||||
|         // Map the values to the names used by the H5P core (it's the same Moodle web does).
 | ||||
|         const content = { | ||||
|             id: contentData.id, | ||||
|             params: contentData.jsoncontent, | ||||
|             embedType: 'iframe', // Always use iframe.
 | ||||
|             disable: null, | ||||
|             folderName: contentData.foldername, | ||||
|             title: libData.title, | ||||
|             slug: CoreH5PCore.slugify(libData.title) + '-' + contentData.id, | ||||
|             filtered: contentData.filtered, | ||||
|             libraryId: libData.id, | ||||
|             libraryName: libData.machinename, | ||||
|             libraryMajorVersion: libData.majorversion, | ||||
|             libraryMinorVersion: libData.minorversion, | ||||
|             libraryEmbedTypes: libData.embedtypes, | ||||
|             libraryFullscreen: libData.fullscreen, | ||||
|             metadata: null, | ||||
|         }; | ||||
| 
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|         const params = CoreTextUtils.instance.parseJSON<any>(contentData.jsoncontent); | ||||
|         if (!params.metadata) { | ||||
|             params.metadata = {}; | ||||
|         } | ||||
|         content.metadata = params.metadata; | ||||
|         content.params = JSON.stringify(typeof params.params != 'undefined' && params.params != null ? params.params : params); | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load dependencies for the given content of the given type. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param type The dependency type. | ||||
|      * @return Content dependencies, indexed by machine name. | ||||
|      */ | ||||
|     async loadContentDependencies( | ||||
|         id: number, | ||||
|         type?: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<{[machineName: string]: CoreH5PContentDependencyData}> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         let query = 'SELECT hl.id AS libraryId, hl.machinename AS machineName, ' + | ||||
|                         'hl.majorversion AS majorVersion, hl.minorversion AS minorVersion, ' + | ||||
|                         'hl.patchversion AS patchVersion, hl.preloadedcss AS preloadedCss, ' + | ||||
|                         'hl.preloadedjs AS preloadedJs, hcl.dropcss AS dropCss, ' + | ||||
|                         'hcl.dependencytype as dependencyType ' + | ||||
|                     'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' + | ||||
|                     'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hcl.libraryid = hl.id ' + | ||||
|                     'WHERE hcl.h5pid = ?'; | ||||
| 
 | ||||
|         const queryArgs: (string | number)[] = []; | ||||
|         queryArgs.push(id); | ||||
| 
 | ||||
|         if (type) { | ||||
|             query += ' AND hcl.dependencytype = ?'; | ||||
|             queryArgs.push(type); | ||||
|         } | ||||
| 
 | ||||
|         query += ' ORDER BY hcl.weight'; | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         const dependencies: {[machineName: string]: CoreH5PContentDependencyData} = {}; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency = result.rows.item(i); | ||||
| 
 | ||||
|             dependencies[dependency.machineName] = dependency; | ||||
|         } | ||||
| 
 | ||||
|         return dependencies; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a library and its dependencies. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @param majorVersion The library's major version. | ||||
|      * @param minorVersion The library's minor version. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data. | ||||
|      */ | ||||
|     async loadLibrary( | ||||
|         machineName: string, | ||||
|         majorVersion: number, | ||||
|         minorVersion: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PLibraryData> { | ||||
| 
 | ||||
|         // First get the library data from DB.
 | ||||
|         const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|         const libraryData: CoreH5PLibraryData = { | ||||
|             libraryId: library.id, | ||||
|             title: library.title, | ||||
|             machineName: library.machinename, | ||||
|             majorVersion: library.majorversion, | ||||
|             minorVersion: library.minorversion, | ||||
|             patchVersion: library.patchversion, | ||||
|             runnable: library.runnable, | ||||
|             fullscreen: library.fullscreen, | ||||
|             embedTypes: library.embedtypes, | ||||
|             preloadedJs: library.preloadedjs || undefined, | ||||
|             preloadedCss: library.preloadedcss || undefined, | ||||
|             dropLibraryCss: library.droplibrarycss || undefined, | ||||
|             semantics: library.semantics || undefined, | ||||
|             preloadedDependencies: [], | ||||
|             dynamicDependencies: [], | ||||
|             editorDependencies: [], | ||||
|         }; | ||||
| 
 | ||||
|         // Now get the dependencies.
 | ||||
|         const sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype ' + | ||||
|                 'FROM ' + LIBRARY_DEPENDENCIES_TABLE_NAME + ' hll ' + | ||||
|                 'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hll.requiredlibraryid = hl.id ' + | ||||
|                 'WHERE hll.libraryid = ? ' + | ||||
|                 'ORDER BY hl.id ASC'; | ||||
| 
 | ||||
|         const sqlParams = [ | ||||
|             library.id, | ||||
|         ]; | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const result = await db.execute(sql, sqlParams); | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency: LibraryDependency = result.rows.item(i); | ||||
|             const key = dependency.dependencytype + 'Dependencies'; | ||||
| 
 | ||||
|             libraryData[key].push({ | ||||
|                 machineName: dependency.machinename, | ||||
|                 majorVersion: dependency.majorversion, | ||||
|                 minorVersion: dependency.minorversion, | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library addon data. | ||||
|      * | ||||
|      * @param library Library addon data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     parseLibAddonData(library: LibraryAddonDBData): CoreH5PLibraryAddonData { | ||||
|         const parsedLib = <CoreH5PLibraryAddonData> library; | ||||
|         parsedLib.addTo = CoreTextUtils.instance.parseJSON<CoreH5PLibraryAddTo | null>(library.addTo, null); | ||||
| 
 | ||||
|         return parsedLib; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library DB data. | ||||
|      * | ||||
|      * @param library Library DB data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     protected parseLibDBData(library: CoreH5PLibraryDBRecord): CoreH5PLibraryParsedDBRecord { | ||||
|         return Object.assign(library, { | ||||
|             semantics: library.semantics ? CoreTextUtils.instance.parseJSON(library.semantics, null) : null, | ||||
|             addto: library.addto ? CoreTextUtils.instance.parseJSON(library.addto, null) : null, | ||||
|             metadatasettings: library.metadatasettings ? CoreTextUtils.instance.parseJSON(library.metadatasettings, null) : null, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resets marked user data for the given content. | ||||
|      * | ||||
|      * @param contentId Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async resetContentUserData(conentId: number, siteId?: string): Promise<void> { | ||||
|         // Currently, we do not store user data for a content.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stores hash keys for cached assets, aggregated JavaScripts and stylesheets, and connects it to libraries so that we | ||||
|      * know which cache file to delete when a library is updated. | ||||
|      * | ||||
|      * @param key Hash key for the given libraries. | ||||
|      * @param libraries List of dependencies used to create the key. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveCachedAssets( | ||||
|         hash: string, | ||||
|         dependencies: {[machineName: string]: CoreH5PContentDependencyData}, | ||||
|         folderName: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(Object.keys(dependencies).map(async (key) => { | ||||
|             const data: Partial<CoreH5PLibraryCachedAssetsDBRecord> = { | ||||
|                 hash: key, | ||||
|                 libraryid: dependencies[key].libraryId, | ||||
|                 foldername: folderName, | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(LIBRARIES_CACHEDASSETS_TABLE_NAME, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save library data in DB. | ||||
|      * | ||||
|      * @param libraryData Library data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryData(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> { | ||||
|         // Some special properties needs some checking and converting before they can be saved.
 | ||||
|         const preloadedJS = this.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'); | ||||
|         const preloadedCSS = this.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'); | ||||
|         const dropLibraryCSS = this.libraryParameterValuesToCsv(libraryData, 'dropLibraryCss', 'machineName'); | ||||
| 
 | ||||
|         if (typeof libraryData.semantics == 'undefined') { | ||||
|             libraryData.semantics = []; | ||||
|         } | ||||
|         if (typeof libraryData.fullscreen == 'undefined') { | ||||
|             libraryData.fullscreen = 0; | ||||
|         } | ||||
| 
 | ||||
|         let embedTypes = ''; | ||||
|         if (typeof libraryData.embedTypes != 'undefined') { | ||||
|             embedTypes = libraryData.embedTypes.join(', '); | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
|         const data: Partial<CoreH5PLibraryDBRecord> = { | ||||
|             title: libraryData.title, | ||||
|             machinename: libraryData.machineName, | ||||
|             majorversion: libraryData.majorVersion, | ||||
|             minorversion: libraryData.minorVersion, | ||||
|             patchversion: libraryData.patchVersion, | ||||
|             runnable: libraryData.runnable, | ||||
|             fullscreen: libraryData.fullscreen, | ||||
|             embedtypes: embedTypes, | ||||
|             preloadedjs: preloadedJS, | ||||
|             preloadedcss: preloadedCSS, | ||||
|             droplibrarycss: dropLibraryCSS, | ||||
|             semantics: typeof libraryData.semantics != 'undefined' ? JSON.stringify(libraryData.semantics) : null, | ||||
|             addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null, | ||||
|             metadatasettings: typeof libraryData.metadataSettings != 'undefined' ? | ||||
|                 CoreH5PMetadata.boolifyAndEncodeSettings(libraryData.metadataSettings) : null, | ||||
|         }; | ||||
| 
 | ||||
|         if (libraryData.libraryId) { | ||||
|             data.id = libraryData.libraryId; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(LIBRARIES_TABLE_NAME, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New library. Get its ID.
 | ||||
|             const entry = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, data); | ||||
| 
 | ||||
|             libraryData.libraryId = entry.id; | ||||
|         } else { | ||||
|             // Updated libary. Remove old dependencies.
 | ||||
|             await this.deleteLibraryDependencies(data.id, site.getId()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save what libraries a library is depending on. | ||||
|      * | ||||
|      * @param libraryId Library Id for the library we're saving dependencies for. | ||||
|      * @param dependencies List of dependencies as associative arrays containing machineName, majorVersion, minorVersion. | ||||
|      * @param dependencytype The type of dependency. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryDependencies( | ||||
|         libraryId: number, | ||||
|         dependencies: CoreH5PLibraryBasicData[], | ||||
|         dependencyType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(dependencies.map(async (dependency) => { | ||||
|             // Get the ID of the library.
 | ||||
|             const dependencyId = await this.getLibraryIdByData(dependency, siteId); | ||||
| 
 | ||||
|             // Create the relation.
 | ||||
|             const entry: Partial<CoreH5PLibraryDependencyDBRecord> = { | ||||
|                 libraryid: libraryId, | ||||
|                 requiredlibraryid: dependencyId, | ||||
|                 dependencytype: dependencyType, | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(LIBRARY_DEPENDENCIES_TABLE_NAME, entry); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Saves what libraries the content uses. | ||||
|      * | ||||
|      * @param id Id identifying the package. | ||||
|      * @param librariesInUse List of libraries the content uses. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryUsage( | ||||
|         id: number, | ||||
|         librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency}, | ||||
|         siteId?: string, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Calculate the CSS to drop.
 | ||||
|         const dropLibraryCssList: Record<string, string> = {}; | ||||
| 
 | ||||
|         for (const key in librariesInUse) { | ||||
|             const dependency = librariesInUse[key]; | ||||
| 
 | ||||
|             if ('dropLibraryCss' in dependency.library && dependency.library.dropLibraryCss) { | ||||
|                 const split = dependency.library.dropLibraryCss.split(', '); | ||||
| 
 | ||||
|                 split.forEach((css) => { | ||||
|                     dropLibraryCssList[css] = css; | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Now save the uusage.
 | ||||
|         await Promise.all(Object.keys(librariesInUse).map((key) => { | ||||
|             const dependency = librariesInUse[key]; | ||||
|             const data: Partial<CoreH5PContentsLibraryDBRecord> = { | ||||
|                 h5pid: id, | ||||
|                 libraryid: dependency.library.libraryId, | ||||
|                 dependencytype: dependency.type, | ||||
|                 dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0, | ||||
|                 weight: dependency.weight, | ||||
|             }; | ||||
| 
 | ||||
|             return db.insertRecord(CONTENTS_LIBRARIES_TABLE_NAME, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with content ID. | ||||
|      */ | ||||
|     async updateContent(content: CoreH5PContentBeingSaved, folderName: string, fileUrl: string, siteId?: string): Promise<number> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // If the libraryid declared in the package is empty, get the latest version.
 | ||||
|         if (content.library && typeof content.library.libraryId == 'undefined') { | ||||
|             const mainLibrary = await this.getLatestLibraryVersion(content.library.machineName, siteId); | ||||
| 
 | ||||
|             content.library.libraryId = mainLibrary.id; | ||||
|         } | ||||
| 
 | ||||
|         const data: Partial<CoreH5PContentDBRecord> = { | ||||
|             id: undefined, | ||||
|             jsoncontent: content.params, | ||||
|             mainlibraryid: content.library?.libraryId, | ||||
|             timemodified: Date.now(), | ||||
|             filtered: null, | ||||
|             foldername: folderName, | ||||
|             fileurl: fileUrl, | ||||
|             timecreated: undefined, | ||||
|         }; | ||||
| 
 | ||||
|         if (typeof content.id != 'undefined') { | ||||
|             data.id = content.id; | ||||
|         } else { | ||||
|             data.timecreated = data.timemodified; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(CONTENT_TABLE_NAME, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New content. Get its ID.
 | ||||
|             const entry = await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, data); | ||||
| 
 | ||||
|             content.id = entry.id; | ||||
|         } | ||||
| 
 | ||||
|         return content.id!; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This will update selected fields on the given content. | ||||
|      * | ||||
|      * @param id Content identifier. | ||||
|      * @param fields Object with the fields to update. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      */ | ||||
|     async updateContentFields(id: number, fields: Partial<CoreH5PContentDBRecord>, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const data = Object.assign({}, fields); | ||||
| 
 | ||||
|         await db.updateRecords(CONTENT_TABLE_NAME, data, { id }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Content data returned by loadContent. | ||||
|  */ | ||||
| export type CoreH5PFrameworkContentData = { | ||||
|     id: number; // The id of the content.
 | ||||
|     params: string; // The content in json format.
 | ||||
|     embedType: string; // Embed type to use.
 | ||||
|     disable: number | null; // H5P Button display options.
 | ||||
|     folderName: string; // Name of the folder that contains the contents.
 | ||||
|     title: string; // Main library's title.
 | ||||
|     slug: string; // Lib title and ID slugified.
 | ||||
|     filtered: string | null; // Filtered version of json_content.
 | ||||
|     libraryId: number; // Main library's ID.
 | ||||
|     libraryName: string; // Main library's machine name.
 | ||||
|     libraryMajorVersion: number; // Main library's major version.
 | ||||
|     libraryMinorVersion: number; // Main library's minor version.
 | ||||
|     libraryEmbedTypes: string; // Main library's list of supported embed types.
 | ||||
|     libraryFullscreen: number; // Main library's display fullscreen button.
 | ||||
|     metadata: unknown; // Content metadata.
 | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PLibraryParsedDBRecord = Omit<CoreH5PLibraryDBRecord, 'semantics'|'addto'|'metadatasettings'> & { | ||||
|     semantics: CoreH5PSemantics[] | null; | ||||
|     addto: CoreH5PLibraryAddTo | null; | ||||
|     metadatasettings: CoreH5PLibraryMetadataSettings | null; | ||||
| }; | ||||
| 
 | ||||
| type LibraryDependency = { | ||||
|     id: number; | ||||
|     machinename: string; | ||||
|     majorversion: number; | ||||
|     minorversion: number; | ||||
|     dependencytype: string; | ||||
| }; | ||||
| 
 | ||||
| type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & { | ||||
|     addTo: string; | ||||
| }; | ||||
| 
 | ||||
							
								
								
									
										255
									
								
								src/core/features/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/core/features/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | ||||
| // (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 { FileEntry } from '@ionic-native/file'; | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreH5P } from '../services/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions } from './core'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P helper class. | ||||
|  */ | ||||
| export class CoreH5PHelper { | ||||
| 
 | ||||
|     /** | ||||
|      * Convert the number representation of display options into an object. | ||||
|      * | ||||
|      * @param displayOptions Number representing display options. | ||||
|      * @return Object with display options. | ||||
|      */ | ||||
|     static decodeDisplayOptions(displayOptions: number): CoreH5PDisplayOptions { | ||||
|         const displayOptionsObject = CoreH5P.instance.h5pCore.getDisplayOptionsAsObject(displayOptions); | ||||
| 
 | ||||
|         const config: CoreH5PDisplayOptions = { | ||||
|             export: false, // Don't allow downloading in the app.
 | ||||
|             embed: false, // Don't display the embed button in the app.
 | ||||
|             copyright: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]) ? | ||||
|                 displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] : false, | ||||
|             icon: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT]) ? | ||||
|                 displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT] : false, | ||||
|         }; | ||||
| 
 | ||||
|         config.frame = config.copyright || config.export || config.embed; | ||||
| 
 | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the core H5P assets, including all core H5P JavaScript and CSS. | ||||
|      * | ||||
|      * @return Array core H5P assets. | ||||
|      */ | ||||
|     static async getCoreAssets( | ||||
|         siteId?: string, | ||||
|     ): Promise<{settings: CoreH5PCoreSettings; cssRequires: string[]; jsRequires: string[]}> { | ||||
| 
 | ||||
|         // Get core settings.
 | ||||
|         const settings = await CoreH5PHelper.getCoreSettings(siteId); | ||||
| 
 | ||||
|         settings.core = { | ||||
|             styles: [], | ||||
|             scripts: [], | ||||
|         }; | ||||
|         settings.loadedJs = []; | ||||
|         settings.loadedCss = []; | ||||
| 
 | ||||
|         const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(); | ||||
|         const cssRequires: string[] = []; | ||||
|         const jsRequires: string[] = []; | ||||
| 
 | ||||
|         // Add core stylesheets.
 | ||||
|         CoreH5PCore.STYLES.forEach((style) => { | ||||
|             settings.core!.styles.push(libUrl + style); | ||||
|             cssRequires.push(libUrl + style); | ||||
|         }); | ||||
| 
 | ||||
|         // Add core JavaScript.
 | ||||
|         CoreH5PCore.getScripts().forEach((script) => { | ||||
|             settings.core!.scripts.push(script); | ||||
|             jsRequires.push(script); | ||||
|         }); | ||||
| 
 | ||||
|         return { settings, cssRequires, jsRequires }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the settings needed by the H5P library. | ||||
|      * | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the settings. | ||||
|      */ | ||||
|     static async getCoreSettings(siteId?: string): Promise<CoreH5PCoreSettings> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const userId = site.getUserId(); | ||||
|         const user = await CoreUtils.instance.ignoreErrors(CoreUser.instance.getProfile(userId, undefined, false, siteId)); | ||||
| 
 | ||||
|         if (!user || !user.email) { | ||||
|             throw new CoreError(Translate.instance.instant('core.h5p.errorgetemail')); | ||||
|         } | ||||
| 
 | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const ajaxPaths = { | ||||
|             xAPIResult: '', | ||||
|             contentUserData: '', | ||||
|         }; | ||||
| 
 | ||||
|         return { | ||||
|             baseUrl: CoreFile.instance.getWWWPath(), | ||||
|             url: CoreFile.instance.convertFileSrc( | ||||
|                 CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, | ||||
|                     CoreH5P.instance.h5pCore.h5pFS.getExternalH5PFolderPath(site.getId()), | ||||
|                 ), | ||||
|             ), | ||||
|             urlLibraries: CoreFile.instance.convertFileSrc( | ||||
|                 CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, | ||||
|                     CoreH5P.instance.h5pCore.h5pFS.getLibrariesFolderPath(site.getId()), | ||||
|                 ), | ||||
|             ), | ||||
|             postUserStatistics: false, | ||||
|             ajax: ajaxPaths, | ||||
|             saveFreq: false, | ||||
|             siteUrl: site.getURL(), | ||||
|             l10n: { | ||||
|                 H5P: CoreH5P.instance.h5pCore.getLocalization(), // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|             }, | ||||
|             user: { name: site.getInfo()!.fullname, mail: user.email }, | ||||
|             hubIsEnabled: false, | ||||
|             reportingIsEnabled: false, | ||||
|             crossorigin: null, | ||||
|             libraryConfig: null, | ||||
|             pluginCacheBuster: '', | ||||
|             libraryUrl: CoreTextUtils.instance.concatenatePaths(CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(), 'js'), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract and store an H5P file. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param onProgress Function to call on progress. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreH5PSaveOnProgress): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Notify that the unzip is starting.
 | ||||
|         onProgress && onProgress({ message: 'core.unzipping' }); | ||||
| 
 | ||||
|         const queueId = siteId + ':saveH5P:' + fileUrl; | ||||
| 
 | ||||
|         await CoreH5P.instance.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, siteId, onProgress)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract and store an H5P file. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param onProgress Function to call on progress. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected static async performSave( | ||||
|         fileUrl: string, | ||||
|         file: FileEntry, | ||||
|         siteId?: string, | ||||
|         onProgress?: CoreH5PSaveOnProgress, | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         const folderName = CoreMimetypeUtils.instance.removeExtension(file.name); | ||||
|         const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
| 
 | ||||
|         // Unzip the file.
 | ||||
|         await CoreFile.instance.unzipFile(file.toURL(), destFolder, onProgress); | ||||
| 
 | ||||
|         try { | ||||
|             // Notify that the unzip is starting.
 | ||||
|             onProgress && onProgress({ message: 'core.storingfiles' }); | ||||
| 
 | ||||
|             // Read the contents of the unzipped dir, process them and store them.
 | ||||
|             const contents = await CoreFile.instance.getDirectoryContents(destFolder); | ||||
| 
 | ||||
|             const filesData = await CoreH5P.instance.h5pValidator.processH5PFiles(destFolder, contents); | ||||
| 
 | ||||
|             const content = await CoreH5P.instance.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId); | ||||
| 
 | ||||
|             // Create the content player.
 | ||||
|             const contentData = await CoreH5P.instance.h5pCore.loadContent(content.id, undefined, siteId); | ||||
| 
 | ||||
|             const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes); | ||||
| 
 | ||||
|             await CoreH5P.instance.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, siteId); | ||||
|         } finally { | ||||
|             // Remove tmp folder.
 | ||||
|             try { | ||||
|                 await CoreFile.instance.removeDir(destFolder); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors, it will be deleted eventually.
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Core settings for H5P. | ||||
|  */ | ||||
| export type CoreH5PCoreSettings = { | ||||
|     baseUrl: string; | ||||
|     url: string; | ||||
|     urlLibraries: string; | ||||
|     postUserStatistics: boolean; | ||||
|     ajax: { | ||||
|         xAPIResult: string; | ||||
|         contentUserData: string; | ||||
|     }; | ||||
|     saveFreq: boolean; | ||||
|     siteUrl: string; | ||||
|     l10n: { | ||||
|         H5P: {[name: string]: string}; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     }; | ||||
|     user: { | ||||
|         name: string; | ||||
|         mail: string; | ||||
|     }; | ||||
|     hubIsEnabled: boolean; | ||||
|     reportingIsEnabled: boolean; | ||||
|     crossorigin: null; | ||||
|     libraryConfig: null; | ||||
|     pluginCacheBuster: string; | ||||
|     libraryUrl: string; | ||||
|     core?: { | ||||
|         styles: string[]; | ||||
|         scripts: string[]; | ||||
|     }; | ||||
|     loadedJs?: string[]; | ||||
|     loadedCss?: string[]; | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PSaveOnProgress = (event?: ProgressEvent | { message: string }) => void; | ||||
							
								
								
									
										45
									
								
								src/core/features/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/core/features/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreH5PLibraryMetadataSettings } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PMetadata class. | ||||
|  */ | ||||
| export class CoreH5PMetadata { | ||||
| 
 | ||||
|     /** | ||||
|      * The metadataSettings field in libraryJson uses 1 for true and 0 for false. | ||||
|      * Here we are converting these to booleans, and also doing JSON encoding. | ||||
|      * | ||||
|      * @param metadataSettings Settings. | ||||
|      * @return Stringified settings. | ||||
|      */ | ||||
|     static boolifyAndEncodeSettings(metadataSettings: CoreH5PLibraryMetadataSettings | string): string { | ||||
|         if (typeof metadataSettings == 'string') { | ||||
|             return metadataSettings; | ||||
|         } | ||||
| 
 | ||||
|         // Convert metadataSettings values to boolean.
 | ||||
|         if (typeof metadataSettings.disable != 'undefined') { | ||||
|             metadataSettings.disable = metadataSettings.disable === 1; | ||||
|         } | ||||
|         if (typeof metadataSettings.disableExtraTitleField != 'undefined') { | ||||
|             metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1; | ||||
|         } | ||||
| 
 | ||||
|         return JSON.stringify(metadataSettings); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										422
									
								
								src/core/features/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										422
									
								
								src/core/features/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,422 @@ | ||||
| // (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 { CoreFile } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreXAPI } from '@features/xapi/services/xapi'; | ||||
| import { CoreH5P } from '../services/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | ||||
| import { CoreH5PCoreSettings, CoreH5PHelper } from './helper'; | ||||
| import { CoreH5PStorage } from './storage'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P player class. | ||||
|  */ | ||||
| export class CoreH5PPlayer { | ||||
| 
 | ||||
|     constructor( | ||||
|         protected h5pCore: CoreH5PCore, | ||||
|         protected h5pStorage: CoreH5PStorage, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculate the URL to the site H5P player. | ||||
|      * | ||||
|      * @param siteUrl Site URL. | ||||
|      * @param fileUrl File URL. | ||||
|      * @param displayOptions Display options. | ||||
|      * @param component Component to send xAPI events to. | ||||
|      * @return URL. | ||||
|      */ | ||||
|     calculateOnlinePlayerUrl(siteUrl: string, fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string): string { | ||||
|         fileUrl = CoreH5P.instance.treatH5PUrl(fileUrl, siteUrl); | ||||
| 
 | ||||
|         const params = this.getUrlParamsFromDisplayOptions(displayOptions); | ||||
|         params.url = encodeURIComponent(fileUrl); | ||||
|         if (component) { | ||||
|             params.component = component; | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php'), params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create the index.html to render an H5P package. | ||||
|      * Part of the code of this function is equivalent to Moodle's add_assets_to_page function. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param h5pUrl The URL of the H5P file. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. The app will always use 'iframe'. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the URL of the index file. | ||||
|      */ | ||||
|     async createContentIndex( | ||||
|         id: number, | ||||
|         h5pUrl: string, | ||||
|         content: CoreH5PContentData, | ||||
|         embedType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<string> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const contentUrl = CoreFile.instance.convertFileSrc( | ||||
|             CoreTextUtils.instance.concatenatePaths( | ||||
|                 basePath, | ||||
|                 this.h5pCore.h5pFS.getContentFolderPath(content.folderName, site.getId()), | ||||
|             ), | ||||
|         ); | ||||
| 
 | ||||
|         // Create the settings needed for the content.
 | ||||
|         const contentSettings = { | ||||
|             library: CoreH5PCore.libraryToString(content.library), | ||||
|             fullScreen: content.library.fullscreen, | ||||
|             exportUrl: '', // We'll never display the download button, so we don't need the exportUrl.
 | ||||
|             embedCode: this.getEmbedCode(site.getURL(), h5pUrl, true), | ||||
|             resizeCode: this.getResizeCode(), | ||||
|             title: content.slug, | ||||
|             displayOptions: {}, | ||||
|             url: '', // It will be filled using dynamic params if needed.
 | ||||
|             contentUrl: contentUrl, | ||||
|             metadata: content.metadata, | ||||
|             contentUserData: [ | ||||
|                 { | ||||
|                     state: '{}', | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
| 
 | ||||
|         // Get the core H5P assets, needed by the H5P classes to render the H5P content.
 | ||||
|         const result = await this.getAssets(id, content, embedType, site.getId()); | ||||
| 
 | ||||
|         result.settings.contents[contentId] = Object.assign(result.settings.contents[contentId], contentSettings); | ||||
| 
 | ||||
|         const indexPath = this.h5pCore.h5pFS.getContentIndexPath(content.folderName, site.getId()); | ||||
|         let html = '<html><head><title>' + content.title + '</title>' + | ||||
|                 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; | ||||
| 
 | ||||
|         // Include the required CSS.
 | ||||
|         result.cssRequires.forEach((cssUrl) => { | ||||
|             html += '<link rel="stylesheet" type="text/css" href="' + cssUrl + '">'; | ||||
|         }); | ||||
| 
 | ||||
|         // Add the settings.
 | ||||
|         html += '<script type="text/javascript">var H5PIntegration = ' + | ||||
|                 JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>'; | ||||
| 
 | ||||
|         // Add our own script to handle the params.
 | ||||
|         html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths( | ||||
|             this.h5pCore.h5pFS.getCoreH5PPath(), | ||||
|             'moodle/js/params.js', | ||||
|         ) + '"></script>'; | ||||
| 
 | ||||
|         html += '</head><body>'; | ||||
| 
 | ||||
|         // Include the required JS at the beginning of the body, like Moodle web does.
 | ||||
|         // Load the embed.js to allow communication with the parent window.
 | ||||
|         html += '<script type="text/javascript" src="' + | ||||
|                 CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/embed.js') + '"></script>'; | ||||
| 
 | ||||
|         result.jsRequires.forEach((jsUrl) => { | ||||
|             html += '<script type="text/javascript" src="' + jsUrl + '"></script>'; | ||||
|         }); | ||||
| 
 | ||||
|         html += '<div class="h5p-iframe-wrapper">' + | ||||
|                 '<iframe id="h5p-iframe-' + id + '" class="h5p-iframe" data-content-id="' + id + '"' + | ||||
|                     'style="height:1px; min-width: 100%" src="about:blank"></iframe>' + | ||||
|                 '</div></body>'; | ||||
| 
 | ||||
|         const fileEntry = await CoreFile.instance.writeFile(indexPath, html); | ||||
| 
 | ||||
|         return fileEntry.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes of all sites from filesystem. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexes(): Promise<void> { | ||||
|         const siteIds = await CoreSites.instance.getSitesIds(); | ||||
| 
 | ||||
|         await Promise.all(siteIds.map((siteId) => this.deleteAllContentIndexesForSite(siteId))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes for a certain site from filesystem. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexesForSite(siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!siteId) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const records = await this.h5pCore.h5pFramework.getAllContentData(siteId); | ||||
| 
 | ||||
|         await Promise.all(records.map(async (record) => { | ||||
|             await CoreUtils.instance.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!)); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all package content data. | ||||
|      * | ||||
|      * @param fileUrl File URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentByUrl(fileUrl: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises([ | ||||
|             this.h5pCore.h5pFramework.deleteContentData(data.id, siteId), | ||||
| 
 | ||||
|             this.h5pCore.h5pFS.deleteContentFolder(data.foldername, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the assets of a package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the assets. | ||||
|      */ | ||||
|     protected async getAssets( | ||||
|         id: number, | ||||
|         content: CoreH5PContentData, | ||||
|         embedType: string, | ||||
|         siteId?: string, | ||||
|     ): Promise<{settings: AssetsSettings; cssRequires: string[]; jsRequires: string[]}> { | ||||
| 
 | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get core assets.
 | ||||
|         const coreAssets = await CoreH5PHelper.getCoreAssets(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const settings = <AssetsSettings> coreAssets.settings; | ||||
|         settings.contents = settings.contents || {}; | ||||
|         settings.contents[contentId] = settings.contents[contentId] || {}; | ||||
| 
 | ||||
|         settings.moodleLibraryPaths = await this.h5pCore.getDependencyRoots(id); | ||||
| 
 | ||||
|         // The Moodle component is added dynamically using the params.js script instead of doing it here.
 | ||||
| 
 | ||||
|         /* The filterParameters function should be called before getting the dependency files because it rebuilds content | ||||
|            dependency cache. */ | ||||
|         settings.contents[contentId].jsonContent = await this.h5pCore.filterParameters(content, siteId); | ||||
| 
 | ||||
|         const files = await this.getDependencyFiles(id, content.folderName, siteId); | ||||
| 
 | ||||
|         // H5P checks the embedType in here, but we'll always use iframe so there's no need to do it.
 | ||||
|         // JavaScripts and stylesheets will be loaded through h5p.js.
 | ||||
|         settings.contents[contentId].scripts = this.h5pCore.getAssetsUrls(files.scripts); | ||||
|         settings.contents[contentId].styles = this.h5pCore.getAssetsUrls(files.styles); | ||||
| 
 | ||||
|         return { | ||||
|             settings: settings, | ||||
|             cssRequires: coreAssets.cssRequires, | ||||
|             jsRequires: coreAssets.jsRequires, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the identifier for the H5P content. This identifier is different than the ID stored in the DB. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @return Content identifier. | ||||
|      */ | ||||
|     protected getContentId(id: number): string { | ||||
|         return 'cid-' + id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param displayOptions Display options. | ||||
|      * @param component Component to send xAPI events to. | ||||
|      * @param contextId Context ID where the H5P is. Required for tracking. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl( | ||||
|         fileUrl: string, | ||||
|         displayOptions?: CoreH5PDisplayOptions, | ||||
|         component?: string, | ||||
|         contextId?: number, | ||||
|         siteId?: string, | ||||
|     ): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         // Add display options and component to the URL.
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         displayOptions = this.h5pCore.fixDisplayOptions(displayOptions || {}, data.id); | ||||
| 
 | ||||
|         const params: Record<string, string> = { | ||||
|             displayOptions: JSON.stringify(displayOptions), | ||||
|             component: component || '', | ||||
|         }; | ||||
| 
 | ||||
|         if (contextId) { | ||||
|             params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId); | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(path, params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finds library dependencies files of a certain package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param folderName Name of the folder of the content. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     protected async getDependencyFiles(id: number, folderName: string, siteId?: string): Promise<CoreH5PDependenciesFiles> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const preloadedDeps = await CoreH5P.instance.h5pCore.loadContentDependencies(id, 'preloaded', siteId); | ||||
| 
 | ||||
|         return this.h5pCore.getDependenciesFiles( | ||||
|             preloadedDeps, | ||||
|             folderName, | ||||
|             this.h5pCore.h5pFS.getExternalH5PFolderPath(siteId), | ||||
|             siteId, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get display options from a URL params. | ||||
|      * | ||||
|      * @param params URL params. | ||||
|      * @return Display options as object. | ||||
|      */ | ||||
|     getDisplayOptionsFromUrlParams(params?: {[name: string]: string}): CoreH5PDisplayOptions { | ||||
|         const displayOptions: CoreH5PDisplayOptions = {}; | ||||
| 
 | ||||
|         if (!params) { | ||||
|             return displayOptions; | ||||
|         } | ||||
| 
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = false; // Never allow downloading in the app.
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = false; // Never show the embed option in the app.
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = | ||||
|                 CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] || | ||||
|                 displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] || displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]; | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] = | ||||
|                 !!this.h5pCore.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true); | ||||
| 
 | ||||
|         return displayOptions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Embed code for settings. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @param embedEnabled Whether the option to embed the H5P content is enabled. | ||||
|      * @return The HTML code to reuse this H5P content in a different place. | ||||
|      */ | ||||
|     protected getEmbedCode(siteUrl: string, h5pUrl: string, embedEnabled?: boolean): string { | ||||
|         if (!embedEnabled) { | ||||
|             return ''; | ||||
|         } | ||||
| 
 | ||||
|         return '<iframe src="' + this.getEmbedUrl(siteUrl, h5pUrl) + '" allowfullscreen="allowfullscreen"></iframe>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the encoded URL for embeding an H5P content. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @return The embed URL. | ||||
|      */ | ||||
|     protected getEmbedUrl(siteUrl: string, h5pUrl: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php') + '?url=' + h5pUrl; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resizing script for settings. | ||||
|      * | ||||
|      * @return The HTML code with the resize script. | ||||
|      */ | ||||
|     protected getResizeCode(): string { | ||||
|         return '<script src="' + this.getResizerScriptUrl() + '"></script>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the URL to the resizer script. | ||||
|      * | ||||
|      * @return URL. | ||||
|      */ | ||||
|     getResizerScriptUrl(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'js/h5p-resizer.js'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get online player URL params from display options. | ||||
|      * | ||||
|      * @param options Display options. | ||||
|      * @return Object with URL params. | ||||
|      */ | ||||
|     getUrlParamsFromDisplayOptions(options?: CoreH5PDisplayOptions): {[name: string]: string} { | ||||
|         const params: {[name: string]: string} = {}; | ||||
| 
 | ||||
|         if (!options) { | ||||
|             return params; | ||||
|         } | ||||
| 
 | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_FRAME] = options[CoreH5PCore.DISPLAY_OPTION_FRAME] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = options[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_EMBED] = options[CoreH5PCore.DISPLAY_OPTION_EMBED] ? '1' : '0'; | ||||
|         params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = options[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] ? '1' : '0'; | ||||
| 
 | ||||
|         return params; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AssetsSettings = CoreH5PCoreSettings & { | ||||
|     contents: { | ||||
|         [contentId: string]: { | ||||
|             jsonContent: string | null; | ||||
|             scripts: string[]; | ||||
|             styles: string[]; | ||||
|         }; | ||||
|     }; | ||||
|     moodleLibraryPaths: { | ||||
|         [libString: string]: string; | ||||
|     }; | ||||
| }; | ||||
							
								
								
									
										233
									
								
								src/core/features/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/core/features/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,233 @@ | ||||
| // (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 { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreH5PCore, CoreH5PLibraryBasicData } from './core'; | ||||
| import { CoreH5PFramework } from './framework'; | ||||
| import { CoreH5PMetadata } from './metadata'; | ||||
| import { | ||||
|     CoreH5PLibrariesJsonData, | ||||
|     CoreH5PLibraryJsonData, | ||||
|     CoreH5PLibraryMetadataSettings, | ||||
|     CoreH5PMainJSONFilesData, | ||||
| } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PStorage class. | ||||
|  */ | ||||
| export class CoreH5PStorage { | ||||
| 
 | ||||
|     constructor( | ||||
|         protected h5pCore: CoreH5PCore, | ||||
|         protected h5pFramework: CoreH5PFramework, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Save libraries. | ||||
|      * | ||||
|      * @param librariesJsonData Data about libraries. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async saveLibraries(librariesJsonData: CoreH5PLibrariesJsonData, folderName: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // First of all, try to create the dir where the libraries are stored. This way we don't have to do it for each lib.
 | ||||
|         await CoreFile.instance.createDir(this.h5pCore.h5pFS.getLibrariesFolderPath(siteId)); | ||||
| 
 | ||||
|         const libraryIds: number[] = []; | ||||
| 
 | ||||
|         // Go through libraries that came with this package.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString]; | ||||
| 
 | ||||
|             // Find local library identifier.
 | ||||
|             const dbData = await CoreUtils.instance.ignoreErrors(this.h5pFramework.getLibraryByData(libraryData)); | ||||
| 
 | ||||
|             if (dbData) { | ||||
|                 // Library already installed.
 | ||||
|                 libraryData.libraryId = dbData.id; | ||||
| 
 | ||||
|                 const isNewPatch = await this.h5pFramework.isPatchedLibrary(libraryData, dbData); | ||||
| 
 | ||||
|                 if (!isNewPatch) { | ||||
|                     // Same or older version, no need to save.
 | ||||
|                     libraryData.saveDependencies = false; | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             libraryData.saveDependencies = true; | ||||
| 
 | ||||
|             // Convert metadataSettings values to boolean and json_encode it before saving.
 | ||||
|             libraryData.metadataSettings = libraryData.metadataSettings ? | ||||
|                 CoreH5PMetadata.boolifyAndEncodeSettings(libraryData.metadataSettings) : undefined; | ||||
| 
 | ||||
|             // Save the library data in DB.
 | ||||
|             await this.h5pFramework.saveLibraryData(libraryData, siteId); | ||||
| 
 | ||||
|             // Now save it in FS.
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveLibrary(libraryData, siteId); | ||||
|             } catch (error) { | ||||
|                 if (libraryData.libraryId) { | ||||
|                     // An error occurred, delete the DB data because the lib FS data has been deleted.
 | ||||
|                     await this.h5pFramework.deleteLibrary(libraryData.libraryId, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             if (typeof libraryData.libraryId != 'undefined') { | ||||
|                 const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|                 // Remove all indexes of contents that use this library.
 | ||||
|                 promises.push(this.h5pCore.h5pFS.deleteContentIndexesForLibrary(libraryData.libraryId, siteId)); | ||||
| 
 | ||||
|                 if (this.h5pCore.aggregateAssets) { | ||||
|                     // Remove cached assets that use this library.
 | ||||
|                     const removedEntries = await this.h5pFramework.deleteCachedAssets(libraryData.libraryId, siteId); | ||||
| 
 | ||||
|                     await this.h5pCore.h5pFS.deleteCachedAssets(removedEntries, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 await CoreUtils.instance.allPromises(promises); | ||||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         // Go through the libraries again to save dependencies.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString]; | ||||
| 
 | ||||
|             if (!libraryData.saveDependencies || !libraryData.libraryId) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const libId = libraryData.libraryId; | ||||
| 
 | ||||
|             libraryIds.push(libId); | ||||
| 
 | ||||
|             // Remove any old dependencies.
 | ||||
|             await this.h5pFramework.deleteLibraryDependencies(libId, siteId); | ||||
| 
 | ||||
|             // Insert the different new ones.
 | ||||
|             const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|             if (typeof libraryData.preloadedDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.preloadedDependencies, 'preloaded')); | ||||
|             } | ||||
|             if (typeof libraryData.dynamicDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.dynamicDependencies, 'dynamic')); | ||||
|             } | ||||
|             if (typeof libraryData.editorDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.editorDependencies, 'editor')); | ||||
|             } | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         })); | ||||
| 
 | ||||
|         // Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries.
 | ||||
|         if (libraryIds.length) { | ||||
|             await this.h5pFramework.clearFilteredParameters(libraryIds, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async savePackage( | ||||
|         data: CoreH5PMainJSONFilesData, | ||||
|         folderName: string, | ||||
|         fileUrl: string, | ||||
|         skipContent?: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreH5PContentBeingSaved> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (this.h5pCore.mayUpdateLibraries()) { | ||||
|             // Save the libraries that were processed.
 | ||||
|             await this.saveLibraries(data.librariesJsonData, folderName, siteId); | ||||
|         } | ||||
| 
 | ||||
|         const content: CoreH5PContentBeingSaved = {}; | ||||
| 
 | ||||
|         if (!skipContent) { | ||||
|             // Find main library version.
 | ||||
|             if (data.mainJsonData.preloadedDependencies) { | ||||
|                 const mainLib = data.mainJsonData.preloadedDependencies.find((dependency) => | ||||
|                     dependency.machineName === data.mainJsonData.mainLibrary); | ||||
| 
 | ||||
|                 if (mainLib) { | ||||
|                     const id = await this.h5pFramework.getLibraryIdByData(mainLib); | ||||
| 
 | ||||
|                     content.library = Object.assign(mainLib, { libraryId: id }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             content.params = JSON.stringify(data.contentJsonData); | ||||
| 
 | ||||
|             // Save the content data in DB.
 | ||||
|             await this.h5pCore.saveContent(content, folderName, fileUrl, siteId); | ||||
| 
 | ||||
|             // Save the content files in their right place in FS.
 | ||||
|             const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
|             const contentPath = CoreTextUtils.instance.concatenatePaths(destFolder, 'content'); | ||||
| 
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveContent(contentPath, folderName, siteId); | ||||
|             } catch (error) { | ||||
|                 // An error occurred, delete the DB data because the content files have been deleted.
 | ||||
|                 await this.h5pFramework.deleteContentData(content.id!, siteId); | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Library to save. | ||||
|  */ | ||||
| export type CoreH5PLibraryBeingSaved = Omit<CoreH5PLibraryJsonData, 'metadataSettings'> & { | ||||
|     libraryId?: number; // Library ID in the DB.
 | ||||
|     saveDependencies?: boolean; // Whether to save dependencies.
 | ||||
|     metadataSettings?: CoreH5PLibraryMetadataSettings | string; // Encoded metadata settings.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data about a content being saved. | ||||
|  */ | ||||
| export type CoreH5PContentBeingSaved = { | ||||
|     id?: number; | ||||
|     params?: string; | ||||
|     library?: CoreH5PContentLibrary; | ||||
| }; | ||||
| 
 | ||||
| export type CoreH5PContentLibrary = CoreH5PLibraryBasicData & { | ||||
|     libraryId?: number; // Library ID in the DB.
 | ||||
| }; | ||||
							
								
								
									
										328
									
								
								src/core/features/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/core/features/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,328 @@ | ||||
| // (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 { CoreFile, CoreFileFormat } from '@services/file'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreH5PSemantics } from './content-validator'; | ||||
| import { CoreH5PCore, CoreH5PLibraryBasicData } from './core'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PValidator class. | ||||
|  */ | ||||
| export class CoreH5PValidator { | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param libDir Directory where the library files are. | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with library data. | ||||
|      */ | ||||
|     protected async getLibraryData(libDir: DirectoryEntry, libPath: string): Promise<CoreH5PLibraryJsonData> { | ||||
| 
 | ||||
|         // Read the required files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readLibraryJsonFile(libPath), | ||||
|             this.readLibrarySemanticsFile(libPath), | ||||
|             this.readLibraryLanguageFiles(libPath), | ||||
|             this.libraryHasIcon(libPath), | ||||
|         ]); | ||||
| 
 | ||||
|         const libraryData: CoreH5PLibraryJsonData = results[0]; | ||||
|         libraryData.semantics = results[1]; | ||||
|         libraryData.language = results[2]; | ||||
|         libraryData.hasIcon = results[3]; | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data for all libraries in an H5P package. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @retun Promise resolved with the libraries data. | ||||
|      */ | ||||
|     protected async getPackageLibrariesData( | ||||
|         packagePath: string, | ||||
|         entries: (DirectoryEntry | FileEntry)[], | ||||
|     ): Promise<CoreH5PLibrariesJsonData> { | ||||
| 
 | ||||
|         const libraries: CoreH5PLibrariesJsonData = {}; | ||||
| 
 | ||||
|         await Promise.all(entries.map(async (entry) => { | ||||
|             if (entry.name[0] == '.' || entry.name[0] == '_' || entry.name == 'content' || entry.isFile) { | ||||
|                 // Skip files, the content folder and any folder starting with a . or _.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const libDirPath = CoreTextUtils.instance.concatenatePaths(packagePath, entry.name); | ||||
| 
 | ||||
|             const libraryData = await this.getLibraryData(<DirectoryEntry> entry, libDirPath); | ||||
| 
 | ||||
|             libraryData.uploadDirectory = libDirPath; | ||||
|             libraries[CoreH5PCore.libraryToString(libraryData)] = libraryData; | ||||
|         })); | ||||
| 
 | ||||
|         return libraries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the library has an icon file. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with boolean: whether the library has an icon file. | ||||
|      */ | ||||
|     protected async libraryHasIcon(libPath: string): Promise<boolean> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'icon.svg'); | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the file exists.
 | ||||
|             await CoreFile.instance.getFile(path); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process libraries from an H5P library, getting the required data to save them. | ||||
|      * This code is inspired on the isValidPackage function in Moodle's H5PValidator. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async processH5PFiles(packagePath: string, entries: (DirectoryEntry | FileEntry)[]): Promise<CoreH5PMainJSONFilesData> { | ||||
| 
 | ||||
|         // Read the needed files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readH5PJsonFile(packagePath), | ||||
|             this.readH5PContentJsonFile(packagePath), | ||||
|             this.getPackageLibrariesData(packagePath, entries), | ||||
|         ]); | ||||
| 
 | ||||
|         return { | ||||
|             librariesJsonData: results[2], | ||||
|             mainJsonData: results[0], | ||||
|             contentJsonData: results[1], | ||||
|         }; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read content.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PContentJsonFile(packagePath: string): Promise<unknown> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'content/content.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read h5p.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PJsonFile(packagePath: string): Promise<CoreH5PMainJSONData> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'h5p.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read library.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readLibraryJsonFile(libPath: string): Promise<CoreH5PLibraryMainJsonData> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'library.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile<CoreH5PLibraryMainJsonData>(path, CoreFileFormat.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read all language files and return their contents indexed by language code. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the language data. | ||||
|      */ | ||||
|     protected async readLibraryLanguageFiles(libPath: string): Promise<CoreH5PLibraryLangsJsonData | undefined> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'language'); | ||||
|             const langIndex: CoreH5PLibraryLangsJsonData = {}; | ||||
| 
 | ||||
|             // Read all the files in the language directory.
 | ||||
|             const entries = await CoreFile.instance.getDirectoryContents(path); | ||||
| 
 | ||||
|             await Promise.all(entries.map(async (entry) => { | ||||
|                 const langFilePath = CoreTextUtils.instance.concatenatePaths(path, entry.name); | ||||
| 
 | ||||
|                 try { | ||||
|                     const langFileData = await CoreFile.instance.readFile<CoreH5PLibraryLangJsonData>( | ||||
|                         langFilePath, | ||||
|                         CoreFileFormat.FORMATJSON, | ||||
|                     ); | ||||
| 
 | ||||
|                     const parts = entry.name.split('.'); // The language code is in parts[0].
 | ||||
|                     langIndex[parts[0]] = langFileData; | ||||
|                 } catch (error) { | ||||
|                     // Ignore this language.
 | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             return langIndex; | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read semantics.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected async readLibrarySemanticsFile(libPath: string): Promise<CoreH5PSemantics[] | undefined> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'semantics.json'); | ||||
| 
 | ||||
|             return await CoreFile.instance.readFile<CoreH5PSemantics[]>(path, CoreFileFormat.FORMATJSON); | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Data of the main JSON H5P files. | ||||
|  */ | ||||
| export type CoreH5PMainJSONFilesData = { | ||||
|     contentJsonData: unknown; // Contents of content.json file.
 | ||||
|     librariesJsonData: CoreH5PLibrariesJsonData; // JSON data about each library.
 | ||||
|     mainJsonData: CoreH5PMainJSONData; // Contents of h5p.json file.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in h5p.json file of a content. More info in https://h5p.org/documentation/developers/json-file-definitions
 | ||||
|  */ | ||||
| export type CoreH5PMainJSONData = { | ||||
|     title: string; // Title of the content.
 | ||||
|     mainLibrary: string; // The main H5P library for this content.
 | ||||
|     language: string; // Language code.
 | ||||
|     preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
 | ||||
|     authors?: { // The name and role of the content authors
 | ||||
|         name: string; | ||||
|         role: string; | ||||
|     }[]; | ||||
|     source?: string; // The source (a URL) of the licensed material.
 | ||||
|     license?: string; // A code for the content license.
 | ||||
|     licenseVersion?: string; // The version of the license above as a string.
 | ||||
|     licenseExtras?: string; // Any additional information about the license.
 | ||||
|     yearFrom?: string; // If a license is valid for a certain period of time, this represents the start year (as a string).
 | ||||
|     yearTo?: string; // If a license is valid for a certain period of time, this represents the end year (as a string).
 | ||||
|     changes?: { // The changelog.
 | ||||
|         date: string; | ||||
|         author: string; | ||||
|         log: string; | ||||
|     }[]; | ||||
|     authorComments?: string; // Comments for the editor of the content.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * All JSON data for libraries of a package. | ||||
|  */ | ||||
| export type CoreH5PLibrariesJsonData = {[libString: string]: CoreH5PLibraryJsonData}; | ||||
| 
 | ||||
| /** | ||||
|  * All JSON data for a library, including semantics and language. | ||||
|  */ | ||||
| export type CoreH5PLibraryJsonData = CoreH5PLibraryMainJsonData & { | ||||
|     semantics?: CoreH5PSemantics[]; // Data in semantics.json.
 | ||||
|     language?: CoreH5PLibraryLangsJsonData; // Language JSON data.
 | ||||
|     hasIcon?: boolean; // Whether the library has an icon.
 | ||||
|     uploadDirectory?: string; // Path where the lib is stored.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in library.json file of a library. More info in https://h5p.org/library-definition
 | ||||
|  */ | ||||
| export type CoreH5PLibraryMainJsonData = { | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     machineName: string; // The library machine name.
 | ||||
|     majorVersion: number; // Major version.
 | ||||
|     minorVersion: number; // Minor version.
 | ||||
|     patchVersion: number; // Patch version.
 | ||||
|     runnable: number; // Whether or not this library is runnable.
 | ||||
|     coreApi?: { // Required version of H5P Core API.
 | ||||
|         majorVersion: number; | ||||
|         minorVersion: number; | ||||
|     }; | ||||
|     author?: string; // The name of the library author.
 | ||||
|     license?: string; // A code for the content license.
 | ||||
|     description?: string; // Textual description of the library.
 | ||||
|     preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     dynamicDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     editorDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     preloadedJs?: { path: string }[]; // List of path to the javascript files required for the library.
 | ||||
|     preloadedCss?: { path: string }[]; // List of path to the CSS files to be loaded with the library.
 | ||||
|     embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
 | ||||
|     fullscreen?: number; // Enables the integrated full-screen button.
 | ||||
|     metadataSettings?: CoreH5PLibraryMetadataSettings; // Metadata settings.
 | ||||
|     addTo?: CoreH5PLibraryAddTo; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library metadata settings. | ||||
|  */ | ||||
| export type CoreH5PLibraryMetadataSettings = { | ||||
|     disable?: boolean | number; | ||||
|     disableExtraTitleField?: boolean | number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library plugin configuration data. | ||||
|  */ | ||||
| export type CoreH5PLibraryAddTo = { | ||||
|     content?: { | ||||
|         types?: { | ||||
|             text?: { | ||||
|                 regex?: string; | ||||
|             }; | ||||
|         }[]; | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in all languages JSON file of a library. | ||||
|  */ | ||||
| export type CoreH5PLibraryLangsJsonData = {[code: string]: CoreH5PLibraryLangJsonData}; | ||||
| 
 | ||||
| /** | ||||
|  * Data stored in each language JSON file of a library. | ||||
|  */ | ||||
| export type CoreH5PLibraryLangJsonData = { | ||||
|     semantics?: CoreH5PSemantics[]; | ||||
| }; | ||||
							
								
								
									
										44
									
								
								src/core/features/h5p/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/core/features/h5p/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| // (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 { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreH5PPlayerComponent } from './h5p-player/h5p-player'; | ||||
| import { CoreH5PIframeComponent } from './h5p-iframe/h5p-iframe'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreH5PPlayerComponent, | ||||
|         CoreH5PIframeComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         CoreDirectivesModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreH5PPlayerComponent, | ||||
|         CoreH5PIframeComponent, | ||||
|     ], | ||||
| }) | ||||
| export class CoreH5PComponentsModule {} | ||||
| @ -0,0 +1,5 @@ | ||||
| <core-loading [hideUntil]="iframeSrc" class="core-loading-center safe-area-page"> | ||||
|     <core-iframe *ngIf="iframeSrc" [src]="iframeSrc" iframeHeight="auto" [allowFullscreen]="true" (loaded)="iframeLoaded()"> | ||||
|     </core-iframe> | ||||
|     <script *ngIf="resizeScript && iframeSrc" type="text/javascript" [src]="resizeScript"></script> | ||||
| </core-loading> | ||||
							
								
								
									
										223
									
								
								src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | ||||
| // (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, Input, Output, ElementRef, OnChanges, SimpleChange, EventEmitter, OnDestroy } from '@angular/core'; | ||||
| import { NavigationEnd, Router } from '@angular/router'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { filter } from 'rxjs/operators'; | ||||
| 
 | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreH5P } from '@features/h5p/services/h5p'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions } from '../../classes/core'; | ||||
| import { CoreH5PHelper } from '../../classes/helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render an iframe with an H5P package. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-h5p-iframe', | ||||
|     templateUrl: 'core-h5p-iframe.html', | ||||
| }) | ||||
| export class CoreH5PIframeComponent implements OnChanges, OnDestroy { | ||||
| 
 | ||||
|     @Input() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
 | ||||
|     @Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
 | ||||
|     @Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
 | ||||
|     @Input() trackComponent?: string; // Component to send xAPI events to.
 | ||||
|     @Input() contextId?: number; // Context ID. Required for tracking.
 | ||||
|     @Output() onIframeUrlSet = new EventEmitter<{src: string; online: boolean}>(); | ||||
|     @Output() onIframeLoaded = new EventEmitter<void>(); | ||||
| 
 | ||||
|     iframeSrc?: string; | ||||
| 
 | ||||
|     protected site: CoreSite; | ||||
|     protected siteId: string; | ||||
|     protected siteCanDownload: boolean; | ||||
|     protected logger: CoreLogger; | ||||
|     protected currentPageRoute?: string; | ||||
|     protected subscription: Subscription; | ||||
|     protected iframeLoadedOnce = false; | ||||
| 
 | ||||
|     constructor( | ||||
|         public elementRef: ElementRef, | ||||
|         router: Router, | ||||
|     ) { | ||||
| 
 | ||||
|         this.logger = CoreLogger.getInstance('CoreH5PIframeComponent'); | ||||
|         this.site = CoreSites.instance.getCurrentSite()!; | ||||
|         this.siteId = this.site.getId(); | ||||
|         this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); | ||||
| 
 | ||||
|         // Send resize events when the page holding this component is re-entered.
 | ||||
|         // @todo: Check that this works as expected.
 | ||||
|         this.currentPageRoute = router.url; | ||||
|         this.subscription = router.events | ||||
|             .pipe(filter(event => event instanceof NavigationEnd)) | ||||
|             .subscribe((event: NavigationEnd) => { | ||||
|                 if (!this.iframeLoadedOnce || event.urlAfterRedirects == this.currentPageRoute) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 window.dispatchEvent(new Event('resize')); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect changes on input properties. | ||||
|      */ | ||||
|     ngOnChanges(changes: {[name: string]: SimpleChange}): void { | ||||
|         // If it's already playing don't change it.
 | ||||
|         if ((changes.fileUrl || changes.onlinePlayerUrl) && !this.iframeSrc) { | ||||
|             this.play(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Play the H5P. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async play(): Promise<void> { | ||||
|         let localUrl: string | undefined; | ||||
|         let state: string; | ||||
| 
 | ||||
|         if (this.fileUrl) { | ||||
|             state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.fileUrl); | ||||
|         } else { | ||||
|             state = CoreConstants.NOT_DOWNLOADABLE; | ||||
|         } | ||||
| 
 | ||||
|         if (this.siteCanDownload && CoreFileHelper.instance.isStateDownloaded(state)) { | ||||
|             // Package is downloaded, use the local URL.
 | ||||
|             localUrl = await this.getLocalUrl(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             if (localUrl) { | ||||
|                 // Local package.
 | ||||
|                 this.iframeSrc = localUrl; | ||||
|             } else { | ||||
|                 this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                     this.site.getURL(), | ||||
|                     this.fileUrl || '', | ||||
|                     this.displayOptions, | ||||
|                     this.trackComponent, | ||||
|                 ); | ||||
| 
 | ||||
|                 // Never allow downloading in the app. This will only work if the user is allowed to change the params.
 | ||||
|                 const src = this.onlinePlayerUrl.replace( | ||||
|                     CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', | ||||
|                     CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=0', | ||||
|                 ); | ||||
| 
 | ||||
|                 // Get auto-login URL so the user is automatically authenticated.
 | ||||
|                 const url = await this.site.getAutoLoginUrl(src, false); | ||||
| 
 | ||||
|                 // Add the preventredirect param so the user can authenticate.
 | ||||
|                 this.iframeSrc = CoreUrlUtils.instance.addParamsToUrl(url, { preventredirect: false }); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading H5P package.', true); | ||||
| 
 | ||||
|         } finally { | ||||
|             this.addResizerScript(); | ||||
|             this.onIframeUrlSet.emit({ src: this.iframeSrc!, online: !!localUrl }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the local URL of the package. | ||||
|      * | ||||
|      * @return Promise resolved with the local URL. | ||||
|      */ | ||||
|     protected async getLocalUrl(): Promise<string | undefined> { | ||||
|         try { | ||||
|             const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl( | ||||
|                 this.fileUrl!, | ||||
|                 this.displayOptions, | ||||
|                 this.trackComponent, | ||||
|                 this.contextId, | ||||
|                 this.siteId, | ||||
|             ); | ||||
| 
 | ||||
|             return url; | ||||
|         } catch (error) { | ||||
|             // Index file doesn't exist, probably deleted because a lib was updated. Try to create it again.
 | ||||
|             try { | ||||
|                 const path = await CoreFilepool.instance.getInternalUrlByUrl(this.siteId, this.fileUrl!); | ||||
| 
 | ||||
|                 const file = await CoreFile.instance.getFile(path); | ||||
| 
 | ||||
|                 await CoreH5PHelper.saveH5P(this.fileUrl!, file, this.siteId); | ||||
| 
 | ||||
|                 // File treated. Try to get the index file URL again.
 | ||||
|                 const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl( | ||||
|                     this.fileUrl!, | ||||
|                     this.displayOptions, | ||||
|                     this.trackComponent, | ||||
|                     this.contextId, | ||||
|                     this.siteId, | ||||
|                 ); | ||||
| 
 | ||||
|                 return url; | ||||
|             } catch (error) { | ||||
|                 // Still failing. Delete the H5P package?
 | ||||
|                 this.logger.error('Error loading downloaded index:', error, this.fileUrl); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add the resizer script if it hasn't been added already. | ||||
|      */ | ||||
|     protected addResizerScript(): void { | ||||
|         if (document.head.querySelector('#core-h5p-resizer-script') != null) { | ||||
|             // Script already added, don't add it again.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const script = document.createElement('script'); | ||||
|         script.id = 'core-h5p-resizer-script'; | ||||
|         script.type = 'text/javascript'; | ||||
|         script.src = CoreH5P.instance.h5pPlayer.getResizerScriptUrl(); | ||||
|         document.head.appendChild(script); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * H5P iframe has been loaded. | ||||
|      */ | ||||
|     iframeLoaded(): void { | ||||
|         this.onIframeLoaded.emit(); | ||||
|         this.iframeLoadedOnce = true; | ||||
| 
 | ||||
|         // Send a resize event to the window so H5P package recalculates the size.
 | ||||
|         window.dispatchEvent(new Event('resize')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.subscription?.unsubscribe(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| <div *ngIf="!showPackage && urlParams" class="core-h5p-placeholder"> | ||||
|     <ion-button class="core-h5p-placeholder-play-button" fill="clear" (click)="play($event)"> | ||||
|         <core-icon name="far-play-circle" slot="icon-only"></core-icon> | ||||
|     </ion-button> | ||||
| 
 | ||||
|     <div class="core-h5p-placeholder-download-container"> | ||||
|         <core-download-refresh [status]="state" [enabled]="canDownload" [loading]="calculating" [canTrustDownload]="true" | ||||
|             (action)="download()"> | ||||
|         </core-download-refresh> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| <core-h5p-iframe *ngIf="showPackage" [fileUrl]="urlParams!.url" [displayOptions]="displayOptions" [onlinePlayerUrl]="src"> | ||||
| </core-h5p-iframe> | ||||
							
								
								
									
										48
									
								
								src/core/features/h5p/components/h5p-player/h5p-player.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/core/features/h5p/components/h5p-player/h5p-player.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| :host { | ||||
|     --core-h5p-placeholder-bg-color: var(--gray); | ||||
|     --core-h5p-placeholder-text-color: var(--ion-text-color); | ||||
| 
 | ||||
|     .core-h5p-placeholder { | ||||
|         position: relative; | ||||
|         width: 100%; | ||||
|         height: 230px; | ||||
|         background: url('../../../../../assets/img/icons/h5p.svg') center top 25px / 100px auto no-repeat var(--core-h5p-placeholder-bg-color); | ||||
|         color: var(--core-h5p-placeholder-text-color); | ||||
| 
 | ||||
|         .icon:not([color="success"]) { | ||||
|             color: var(--core-h5p-placeholder-text-color); | ||||
|         } | ||||
| 
 | ||||
|         .core-h5p-placeholder-play-button, .core-h5p-placeholder-spinner { | ||||
|             position: absolute; | ||||
|             top: 50%; | ||||
|             left: 50%; | ||||
|             transform: translate(-50%, -50%); | ||||
|         } | ||||
| 
 | ||||
|         .core-h5p-placeholder-play-button { | ||||
|             font-size: 30px; | ||||
|             min-height: 50px; | ||||
|         } | ||||
| 
 | ||||
|         .core-h5p-placeholder-download-container { | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             right: 0; | ||||
| 
 | ||||
|             ion-spinner { | ||||
|                 margin-right: 0.75em; | ||||
|             } | ||||
| 
 | ||||
|             core-download-refresh > ion-icon { | ||||
|                 margin: 0.4rem 0.2rem; | ||||
|                 padding: 0 0.5em; | ||||
|                 line-height: .67; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         ion-spinner circle { | ||||
|             stroke: var(--core-h5p-placeholder-text-color); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										219
									
								
								src/core/features/h5p/components/h5p-player/h5p-player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/core/features/h5p/components/h5p-player/h5p-player.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | ||||
| // (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, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreEvents, CoreEventObserver } from '@singletons/events'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreH5P } from '@features/h5p/services/h5p'; | ||||
| import { CoreH5PDisplayOptions } from '../../classes/core'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render an H5P package. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-h5p-player', | ||||
|     templateUrl: 'core-h5p-player.html', | ||||
|     styleUrls: ['h5p-player.scss'], | ||||
| }) | ||||
| export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|     @Input() src?: string; // The URL of the player to display the H5P package.
 | ||||
|     @Input() component?: string; // Component.
 | ||||
|     @Input() componentId?: string | number; // Component ID to use in conjunction with the component.
 | ||||
| 
 | ||||
|     showPackage = false; | ||||
|     state?: string; | ||||
|     canDownload = false; | ||||
|     calculating = true; | ||||
|     displayOptions?: CoreH5PDisplayOptions; | ||||
|     urlParams?: {[name: string]: string}; | ||||
| 
 | ||||
|     protected site: CoreSite; | ||||
|     protected siteId: string; | ||||
|     protected siteCanDownload: boolean; | ||||
|     protected observer?: CoreEventObserver; | ||||
|     protected logger: CoreLogger; | ||||
| 
 | ||||
|     constructor( | ||||
|         public elementRef: ElementRef, | ||||
|     ) { | ||||
| 
 | ||||
|         this.logger = CoreLogger.getInstance('CoreH5PPlayerComponent'); | ||||
|         this.site = CoreSites.instance.getCurrentSite()!; | ||||
|         this.siteId = this.site.getId(); | ||||
|         this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.checkCanDownload(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect changes on input properties. | ||||
|      */ | ||||
|     ngOnChanges(changes: {[name: string]: SimpleChange}): void { | ||||
|         // If it's already playing there's no need to check if it can be downloaded.
 | ||||
|         if (changes.src && !this.showPackage) { | ||||
|             this.checkCanDownload(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Play the H5P. | ||||
|      * | ||||
|      * @param e Event. | ||||
|      */ | ||||
|     async play(e: MouseEvent): Promise<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         this.displayOptions = CoreH5P.instance.h5pPlayer.getDisplayOptionsFromUrlParams(this.urlParams); | ||||
|         this.showPackage = true; | ||||
| 
 | ||||
|         if (!this.canDownload || (this.state != CoreConstants.OUTDATED && this.state != CoreConstants.NOT_DOWNLOADED)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Download the package in background if the size is low.
 | ||||
|         try { | ||||
|             this.attemptDownloadInBg(); | ||||
|         } catch (error) { | ||||
|             this.logger.error('Error downloading H5P in background', error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the package. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async download(): Promise<void> { | ||||
|         if (!CoreApp.instance.isOnline()) { | ||||
|             CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Get the file size and ask the user to confirm.
 | ||||
|             const size = await CorePluginFileDelegate.instance.getFileSize({ fileurl: this.urlParams!.url }, this.siteId); | ||||
| 
 | ||||
|             await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true }); | ||||
| 
 | ||||
|             // User confirmed, add to the queue.
 | ||||
|             await CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams!.url, this.component, this.componentId); | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             if (CoreDomUtils.instance.isCanceledError(error)) { | ||||
|                 // User cancelled, stop.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); | ||||
|             this.calculateState(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the H5P in background if the size is low. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async attemptDownloadInBg(): Promise<void> { | ||||
|         if (!this.urlParams || !this.src || !this.siteCanDownload || !CoreH5P.instance.canGetTrustedH5PFileInSite() || | ||||
|                 !CoreApp.instance.isOnline()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Get the file size.
 | ||||
|         const size = await CorePluginFileDelegate.instance.getFileSize({ fileurl: this.urlParams.url }, this.siteId); | ||||
| 
 | ||||
|         if (CoreFilepool.instance.shouldDownload(size)) { | ||||
|             // Download the file in background.
 | ||||
|             CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the package can be downloaded. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async checkCanDownload(): Promise<void> { | ||||
|         this.observer && this.observer.off(); | ||||
|         this.urlParams = CoreUrlUtils.instance.extractUrlParams(this.src || ''); | ||||
| 
 | ||||
|         if (this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) { | ||||
|             this.calculateState(); | ||||
| 
 | ||||
|             // Listen for changes in the state.
 | ||||
|             try { | ||||
|                 const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.urlParams.url); | ||||
| 
 | ||||
|                 this.observer = CoreEvents.on(eventName, () => { | ||||
|                     this.calculateState(); | ||||
|                 }); | ||||
|             } catch (error) { | ||||
|                 // An error probably means the file cannot be downloaded or we cannot check it (offline).
 | ||||
|             } | ||||
| 
 | ||||
|         } else { | ||||
|             this.calculating = false; | ||||
|             this.canDownload = false; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculate state of the file. | ||||
|      * | ||||
|      * @param fileUrl The H5P file URL. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async calculateState(): Promise<void> { | ||||
|         this.calculating = true; | ||||
| 
 | ||||
|         // Get the status of the file.
 | ||||
|         try { | ||||
|             const state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.urlParams!.url); | ||||
| 
 | ||||
|             this.canDownload = true; | ||||
|             this.state = state; | ||||
|         } catch (error) { | ||||
|             this.canDownload = false; | ||||
|         } finally { | ||||
|             this.calculating = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.observer?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										55
									
								
								src/core/features/h5p/h5p.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/core/features/h5p/h5p.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| // (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 { CorePluginFileDelegate } from '@services/plugin-file-delegate'; | ||||
| import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { CoreH5PComponentsModule } from './components/components.module'; | ||||
| import { | ||||
|     CONTENT_TABLE_NAME, | ||||
|     LIBRARIES_TABLE_NAME, | ||||
|     LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|     CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|     LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
| } from './services/database/h5p'; | ||||
| import { CoreH5PPluginFileHandler } from './services/handlers/pluginfile'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreH5PComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [ | ||||
|                 CONTENT_TABLE_NAME, | ||||
|                 LIBRARIES_TABLE_NAME, | ||||
|                 LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|                 CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|                 LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             ], | ||||
|             multi: true, | ||||
|         }, | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => () => { | ||||
|                 CorePluginFileDelegate.instance.registerHandler(CoreH5PPluginFileHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class CoreH5PModule {} | ||||
							
								
								
									
										93
									
								
								src/core/features/h5p/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/core/features/h5p/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| { | ||||
|     "additionallicenseinfo": "Any additional information about the licence", | ||||
|     "author": "Author", | ||||
|     "authorcomments": "Author comments", | ||||
|     "authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)", | ||||
|     "authorname": "Author's name", | ||||
|     "authorrole": "Author's role", | ||||
|     "by": "by", | ||||
|     "cancellabel": "Cancel", | ||||
|     "ccattribution": "Attribution (CC BY)", | ||||
|     "ccattributionnc": "Attribution-NonCommercial (CC BY-NC)", | ||||
|     "ccattributionncnd": "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)", | ||||
|     "ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)", | ||||
|     "ccattributionnd": "Attribution-NoDerivs (CC BY-ND)", | ||||
|     "ccattributionsa": "Attribution-ShareAlike (CC BY-SA)", | ||||
|     "ccpdd": "Public Domain Dedication (CC0)", | ||||
|     "changedby": "Changed by", | ||||
|     "changedescription": "Description of change", | ||||
|     "changelog": "Changelog", | ||||
|     "changeplaceholder": "Photo cropped, text changed, etc.", | ||||
|     "close": "Close", | ||||
|     "confirmdialogbody": "Please confirm that you wish to proceed. This action cannot be undone.", | ||||
|     "confirmdialogheader": "Confirm action", | ||||
|     "confirmlabel": "Confirm", | ||||
|     "connectionLost": "Connection lost. Results will be stored and sent when the connection is reestablished.", | ||||
|     "connectionReestablished": "Connection reestablished.", | ||||
|     "contentCopied": "Content is copied to the clipboard", | ||||
|     "contentchanged": "This content has changed since you last used it.", | ||||
|     "contenttype": "Content type", | ||||
|     "copyright": "Rights of use", | ||||
|     "copyrightinfo": "Copyright information", | ||||
|     "copyrightstring": "Copyright", | ||||
|     "copyrighttitle": "View copyright information for this content.", | ||||
|     "creativecommons": "Creative Commons", | ||||
|     "date": "Date", | ||||
|     "disablefullscreen": "Disable fullscreen", | ||||
|     "download": "Download", | ||||
|     "downloadtitle": "Download this content as a H5P file.", | ||||
|     "editor": "Editor", | ||||
|     "embed": "Embed", | ||||
|     "embedtitle": "View the embed code for this content.", | ||||
|     "errorgetemail": "Error obtaining the user email. Please check your connection and try again.", | ||||
|     "fullscreen": "Fullscreen", | ||||
|     "gpl": "General Public License v3", | ||||
|     "h5ptitle": "Visit h5p.org to check out more content.", | ||||
|     "hideadvanced": "Hide advanced", | ||||
|     "license": "Licence", | ||||
|     "licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", | ||||
|     "licenseCC010U": "CC0 1.0 Universal", | ||||
|     "licenseCC10": "1.0 Generic", | ||||
|     "licenseCC20": "2.0 Generic", | ||||
|     "licenseCC25": "2.5 Generic", | ||||
|     "licenseCC30": "3.0 Unported", | ||||
|     "licenseCC40": "4.0 International", | ||||
|     "licenseGPL": "General Public License", | ||||
|     "licenseV1": "Version 1", | ||||
|     "licenseV2": "Version 2", | ||||
|     "licenseV3": "Version 3", | ||||
|     "licensee": "Licensee", | ||||
|     "licenseextras": "Licence extras", | ||||
|     "licenseversion": "Licence version", | ||||
|     "nocopyright": "No copyright information available for this content.", | ||||
|     "offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.", | ||||
|     "offlineDialogHeader": "Your connection to the server was lost", | ||||
|     "offlineDialogRetryButtonLabel": "Retry now", | ||||
|     "offlineDialogRetryMessage": "Retrying in :num....", | ||||
|     "offlineSuccessfulSubmit": "Successfully submitted results.", | ||||
|     "offlinedisabled": "The site doesn't allow downloading H5P packages.", | ||||
|     "originator": "Originator", | ||||
|     "pd": "Public Domain", | ||||
|     "pddl": "Public Domain Dedication and Licence", | ||||
|     "pdm": "Public Domain Mark (PDM)", | ||||
|     "play": "Play H5P", | ||||
|     "resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:", | ||||
|     "resubmitScores": "Attempting to submit stored results.", | ||||
|     "reuse": "Reuse", | ||||
|     "reuseContent": "Reuse content", | ||||
|     "reuseDescription": "Reuse this content.", | ||||
|     "showadvanced": "Show advanced", | ||||
|     "showless": "Show less", | ||||
|     "showmore": "Show more", | ||||
|     "size": "Size", | ||||
|     "source": "Source", | ||||
|     "startingover": "You'll be starting over.", | ||||
|     "sublevel": "Sublevel", | ||||
|     "thumbnail": "Thumbnail", | ||||
|     "title": "Title", | ||||
|     "undisclosed": "Undisclosed", | ||||
|     "year": "Year", | ||||
|     "years": "Year(s)", | ||||
|     "yearsfrom": "Years (from)", | ||||
|     "yearsto": "Years (to)" | ||||
| } | ||||
							
								
								
									
										336
									
								
								src/core/features/h5p/services/database/h5p.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/core/features/h5p/services/database/h5p.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,336 @@ | ||||
| // (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 { SQLiteDB } from '@classes/sqlitedb'; | ||||
| import { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for CoreH5PProvider service. | ||||
|  */ | ||||
| // DB table names.
 | ||||
| export const CONTENT_TABLE_NAME = 'h5p_content'; // H5P content.
 | ||||
| export const LIBRARIES_TABLE_NAME = 'h5p_libraries_2'; // Installed libraries.
 | ||||
| export const LIBRARY_DEPENDENCIES_TABLE_NAME = 'h5p_library_dependencies'; // Library dependencies.
 | ||||
| export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which library is used in which content.
 | ||||
| export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
 | ||||
| export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'CoreH5PProvider', | ||||
|     version: 1, | ||||
|     canBeCleared: [ | ||||
|         CONTENT_TABLE_NAME, | ||||
|         LIBRARIES_TABLE_NAME, | ||||
|         LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|         CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|         LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|     ], | ||||
|     tables: [ | ||||
|         { | ||||
|             name: CONTENT_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'jsoncontent', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'mainlibraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'foldername', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'fileurl', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'filtered', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'machinename', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'title', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'majorversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'minorversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'patchversion', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'runnable', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'fullscreen', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'embedtypes', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'preloadedjs', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'preloadedcss', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'droplibrarycss', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'semantics', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'addto', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'metadatasettings', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARY_DEPENDENCIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'requiredlibraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dependencytype', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: CONTENTS_LIBRARIES_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'h5pid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dependencytype', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'dropcss', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'weight', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             name: LIBRARIES_CACHEDASSETS_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'libraryid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'hash', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'foldername', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|     ], | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> { | ||||
|         if (oldVersion >= 2) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const newTable = LIBRARIES_TABLE_NAME; | ||||
|         const oldTable = 'h5p_libraries'; | ||||
| 
 | ||||
|         try { | ||||
|             await db.tableExists(oldTable); | ||||
| 
 | ||||
|             // Move the records from the old table.
 | ||||
|             const entries = await db.getAllRecords<CoreH5PLibraryDBRecord>(oldTable); | ||||
| 
 | ||||
|             await Promise.all(entries.map((entry) => db.insertRecord(newTable, entry))); | ||||
| 
 | ||||
|             await db.dropTable(oldTable); | ||||
|         } catch { | ||||
|             // Old table does not exist, ignore.
 | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of content data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PContentDBRecord = { | ||||
|     id: number; // The id of the content.
 | ||||
|     jsoncontent: string; // The content in json format.
 | ||||
|     mainlibraryid: number; // The library we first instantiate for this node.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
|     fileurl: string; // The online URL of the H5P package.
 | ||||
|     filtered: string | null; // Filtered version of json_content.
 | ||||
|     timecreated: number; // Time created.
 | ||||
|     timemodified: number; // Time modified.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDBRecord = { | ||||
|     id: number; // The id of the library.
 | ||||
|     machinename: string; // The library machine name.
 | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     majorversion: number; // Major version.
 | ||||
|     minorversion: number; // Minor version.
 | ||||
|     patchversion: number; // Patch version.
 | ||||
|     runnable: number; // Can this library be started by the module? I.e. not a dependency.
 | ||||
|     fullscreen: number; // Display fullscreen button.
 | ||||
|     embedtypes: string; // List of supported embed types.
 | ||||
|     preloadedjs?: string | null; // Comma separated list of scripts to load.
 | ||||
|     preloadedcss?: string | null; // Comma separated list of stylesheets to load.
 | ||||
|     droplibrarycss?: string | null; // Libraries that should not have CSS included if this lib is used. Comma separated list.
 | ||||
|     semantics?: string | null; // The semantics definition.
 | ||||
|     addto?: string | null; // Plugin configuration data.
 | ||||
|     metadatasettings?: string | null; // Metadata settings.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library dependencies stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDependencyDBRecord = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     requiredlibraryid: number; // The dependent library to load.
 | ||||
|     dependencytype: string; // Type: preloaded, dynamic, or editor.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library used by a content stored in DB. | ||||
|  */ | ||||
| export type CoreH5PContentsLibraryDBRecord = { | ||||
|     id: number; | ||||
|     h5pid: number; | ||||
|     libraryid: number; | ||||
|     dependencytype: string; | ||||
|     dropcss: number; | ||||
|     weight: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of library cached assets stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryCachedAssetsDBRecord = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     hash: string; // The hash to identify the cached asset.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
| }; | ||||
							
								
								
									
										248
									
								
								src/core/features/h5p/services/h5p.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								src/core/features/h5p/services/h5p.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,248 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreQueueRunner } from '@classes/queue-runner'; | ||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||
| 
 | ||||
| import { CoreH5PCore } from '../classes/core'; | ||||
| import { CoreH5PFramework } from '../classes/framework'; | ||||
| import { CoreH5PPlayer } from '../classes/player'; | ||||
| import { CoreH5PStorage } from '../classes/storage'; | ||||
| import { CoreH5PValidator } from '../classes/validator'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to provide H5P functionalities. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreH5PProvider { | ||||
| 
 | ||||
|     h5pCore: CoreH5PCore; | ||||
|     h5pFramework: CoreH5PFramework; | ||||
|     h5pPlayer: CoreH5PPlayer; | ||||
|     h5pStorage: CoreH5PStorage; | ||||
|     h5pValidator: CoreH5PValidator; | ||||
|     queueRunner: CoreQueueRunner; | ||||
| 
 | ||||
|     protected readonly ROOT_CACHE_KEY = 'CoreH5P:'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.queueRunner = new CoreQueueRunner(1); | ||||
| 
 | ||||
|         this.h5pValidator = new CoreH5PValidator(); | ||||
|         this.h5pFramework = new CoreH5PFramework(); | ||||
|         this.h5pCore = new CoreH5PCore(this.h5pFramework); | ||||
|         this.h5pStorage = new CoreH5PStorage(this.h5pCore, this.h5pFramework); | ||||
|         this.h5pPlayer = new CoreH5PPlayer(this.h5pCore, this.h5pStorage); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to get trusted H5P file is available. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.8 | ||||
|      */ | ||||
|     async canGetTrustedH5PFile(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return this.canGetTrustedH5PFileInSite(site); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to get trusted H5P file is available in a certain site. | ||||
|      * | ||||
|      * @param site Site. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.8 | ||||
|      */ | ||||
|     canGetTrustedH5PFileInSite(site?: CoreSite): boolean { | ||||
|         site = site || CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!(site?.wsAvailable('core_h5p_get_trusted_h5p_file')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a trusted H5P file. | ||||
|      * | ||||
|      * @param url The file URL. | ||||
|      * @param options Options. | ||||
|      * @param ignoreCache Whether to ignore cache. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file data. | ||||
|      */ | ||||
|     async getTrustedH5PFile( | ||||
|         url: string, | ||||
|         options?: CoreH5PGetTrustedFileOptions, | ||||
|         ignoreCache?: boolean, | ||||
|         siteId?: string, | ||||
|     ): Promise<CoreWSExternalFile> { | ||||
| 
 | ||||
|         options = options || {}; | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const data: CoreH5pGetTrustedH5pFileWSParams = { | ||||
|             url: this.treatH5PUrl(url, site.getURL()), | ||||
|             frame: options.frame ? 1 : 0, | ||||
|             export: options.export ? 1 : 0, | ||||
|             embed: options.embed ? 1 : 0, | ||||
|             copyright: options.copyright ? 1 : 0, | ||||
|         }; | ||||
|         const preSets: CoreSiteWSPreSets = { | ||||
|             cacheKey: this.getTrustedH5PFileCacheKey(url), | ||||
|             updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||
|         }; | ||||
| 
 | ||||
|         if (ignoreCache) { | ||||
|             preSets.getFromCache = false; | ||||
|             preSets.emergencyCache = false; | ||||
|         } | ||||
| 
 | ||||
|         const result: CoreH5PGetTrustedH5PFileResult = await site.read('core_h5p_get_trusted_h5p_file', data, preSets); | ||||
| 
 | ||||
|         if (result.warnings && result.warnings.length) { | ||||
|             throw result.warnings[0]; | ||||
|         } | ||||
| 
 | ||||
|         if (result.files && result.files.length) { | ||||
|             return result.files[0]; | ||||
|         } | ||||
| 
 | ||||
|         throw new CoreError('File not found'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for trusted H5P file WS calls. | ||||
|      * | ||||
|      * @param url The file URL. | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     protected getTrustedH5PFileCacheKey(url: string): string { | ||||
|         return this.getTrustedH5PFilePrefixCacheKey() + url; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get prefixed cache key for trusted H5P file WS calls. | ||||
|      * | ||||
|      * @return Cache key. | ||||
|      */ | ||||
|     protected getTrustedH5PFilePrefixCacheKey(): string { | ||||
|         return this.ROOT_CACHE_KEY + 'trustedH5PFile:'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates all trusted H5P file WS calls. | ||||
|      * | ||||
|      * @param siteId Site ID (empty for current site). | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateAllGetTrustedH5PFile(siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates get trusted H5P file WS call. | ||||
|      * | ||||
|      * @param url The URL of the file. | ||||
|      * @param siteId Site ID (empty for current site). | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether H5P offline is disabled. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: whether is disabled. | ||||
|      */ | ||||
|     async isOfflineDisabled(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return this.isOfflineDisabledInSite(site); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether H5P offline is disabled. | ||||
|      * | ||||
|      * @param site Site instance. If not defined, current site. | ||||
|      * @return Whether is disabled. | ||||
|      */ | ||||
|     isOfflineDisabledInSite(site?: CoreSite): boolean { | ||||
|         site = site || CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!(site?.isFeatureDisabled('NoDelegate_H5POffline')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat an H5P url before sending it to WS. | ||||
|      * | ||||
|      * @param url H5P file URL. | ||||
|      * @param siteUrl Site URL. | ||||
|      * @return Treated url. | ||||
|      */ | ||||
|     treatH5PUrl(url: string, siteUrl: string): string { | ||||
|         if (url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) { | ||||
|             url = url.replace('/webservice/pluginfile', '/pluginfile'); | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.removeUrlParams(url); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreH5P extends makeSingleton(CoreH5PProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_h5p_get_trusted_h5p_file WS. | ||||
|  */ | ||||
| export type CoreH5pGetTrustedH5pFileWSParams = { | ||||
|     url: string; // H5P file url.
 | ||||
|     frame?: number; // The frame allow to show the bar options below the content.
 | ||||
|     export?: number; // The export allow to download the package.
 | ||||
|     embed?: number; // The embed allow to copy the code to your site.
 | ||||
|     copyright?: number; // The copyright option.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Options for core_h5p_get_trusted_h5p_file. | ||||
|  */ | ||||
| export type CoreH5PGetTrustedFileOptions = { | ||||
|     frame?: boolean; // Whether to show the bar options below the content.
 | ||||
|     export?: boolean; // Whether to allow to download the package.
 | ||||
|     embed?: boolean; // Whether to allow to copy the code to your site.
 | ||||
|     copyright?: boolean; // The copyright option.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Result of core_h5p_get_trusted_h5p_file. | ||||
|  */ | ||||
| export type CoreH5PGetTrustedH5PFileResult = { | ||||
|     files: CoreWSExternalFile[]; // Files.
 | ||||
|     warnings: CoreWSExternalWarning[]; // List of warnings.
 | ||||
| }; | ||||
							
								
								
									
										173
									
								
								src/core/features/h5p/services/handlers/pluginfile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/core/features/h5p/services/handlers/pluginfile.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { FileEntry } from '@ionic-native/file'; | ||||
| 
 | ||||
| import { CoreFilepoolOnProgressCallback } from '@services/filepool'; | ||||
| import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWSExternalFile } from '@services/ws'; | ||||
| import { CoreH5P } from '../h5p'; | ||||
| import { Translate, makeSingleton } from '@singletons'; | ||||
| import { CoreH5PHelper } from '../../classes/helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat H5P files. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { | ||||
| 
 | ||||
|     name = 'CoreH5PPluginFileHandler'; | ||||
| 
 | ||||
|     /** | ||||
|      * React to a file being deleted. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param path The path of the deleted file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<void> { | ||||
|         // If an h5p file is deleted, remove the contents folder.
 | ||||
|         await CoreH5P.instance.h5pPlayer.deleteContentByUrl(fileUrl, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a file can be downloaded. If so, return the file to download. | ||||
|      * | ||||
|      * @param file The file data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file to use. Rejected if cannot download. | ||||
|      */ | ||||
|     async getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         if (site.containsUrl(file.fileurl) && file.fileurl.match(/pluginfile\.php\/[^/]+\/core_h5p\/export\//i)) { | ||||
|             // It's already a deployed file, use it.
 | ||||
|             return file; | ||||
|         } | ||||
| 
 | ||||
|         return CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by | ||||
|      * CoreFilepoolProvider.extractDownloadableFilesFromHtml. | ||||
|      * | ||||
|      * @param container Container where to get the URLs from. | ||||
|      * @return List of URLs. | ||||
|      */ | ||||
|     getDownloadableFilesFromHTML(container: HTMLElement): string[] { | ||||
|         const iframes = <HTMLIFrameElement[]> Array.from(container.querySelectorAll('iframe.h5p-iframe')); | ||||
|         const urls: string[] = []; | ||||
| 
 | ||||
|         for (let i = 0; i < iframes.length; i++) { | ||||
|             const params = CoreUrlUtils.instance.extractUrlParams(iframes[i].src); | ||||
| 
 | ||||
|             if (params.url) { | ||||
|                 urls.push(params.url); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return urls; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a file size. | ||||
|      * | ||||
|      * @param file The file data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the size. | ||||
|      */ | ||||
|     async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> { | ||||
|         try { | ||||
|             const trustedFile = await this.getDownloadableFile(file, siteId); | ||||
| 
 | ||||
|             return trustedFile.filesize || 0; | ||||
|         } catch (error) { | ||||
|             if (CoreUtils.instance.isWebServiceError(error)) { | ||||
|                 // WS returned an error, it means it cannot be downloaded.
 | ||||
|                 return 0; | ||||
|             } | ||||
| 
 | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return CoreH5P.instance.canGetTrustedH5PFileInSite(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a file is downloadable. | ||||
|      * | ||||
|      * @param file The file data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. | ||||
|      */ | ||||
|     async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<CorePluginFileDownloadableResult> { | ||||
|         const offlineDisabled = await CoreH5P.instance.isOfflineDisabled(siteId); | ||||
| 
 | ||||
|         if (offlineDisabled) { | ||||
|             return { | ||||
|                 downloadable: false, | ||||
|                 reason: Translate.instance.instant('core.h5p.offlinedisabled'), | ||||
|             }; | ||||
|         } else { | ||||
|             return { | ||||
|                 downloadable: true, | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the file should be treated by this handler. It is used in functions where the component isn't used. | ||||
|      * | ||||
|      * @param file The file data. | ||||
|      * @return Whether the file should be treated by this handler. | ||||
|      */ | ||||
|     shouldHandleFile(file: CoreWSExternalFile): boolean { | ||||
|         return CoreMimetypeUtils.instance.guessExtensionFromUrl(file.fileurl) == 'h5p'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat a downloaded file. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param onProgress Function to call on progress. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     treatDownloadedFile( | ||||
|         fileUrl: string, | ||||
|         file: FileEntry, | ||||
|         siteId?: string, | ||||
|         onProgress?: CoreFilepoolOnProgressCallback, | ||||
|     ): Promise<void> { | ||||
|         return CoreH5PHelper.saveH5P(fileUrl, file, siteId, onProgress); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreH5PPluginFileHandler extends makeSingleton(CoreH5PPluginFileHandlerService) {} | ||||
							
								
								
									
										74
									
								
								src/core/features/xapi/services/database/xapi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/core/features/xapi/services/database/xapi.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| // (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 { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Database variables for CoreXAPIOfflineProvider service. | ||||
|  */ | ||||
| export const STATEMENTS_TABLE_NAME = 'core_xapi_statements'; | ||||
| export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'CoreXAPIOfflineProvider', | ||||
|     version: 1, | ||||
|     tables: [ | ||||
|         { | ||||
|             name: STATEMENTS_TABLE_NAME, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true, | ||||
|                     autoIncrement: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'contextid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'component', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'statements', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'extra', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Structure of statement data stored in DB. | ||||
|  */ | ||||
| export type CoreXAPIStatementDBRecord = { | ||||
|     id: number; // ID.
 | ||||
|     contextid: number; // Context ID of the statements.
 | ||||
|     component: string; // Component to send the statements to.
 | ||||
|     statements: string; // Statements (JSON-encoded).
 | ||||
|     timecreated: number; // When were the statements created.
 | ||||
|     courseid?: number; // Course ID if the context is inside a course.
 | ||||
|     extra?: string; // Extra data.
 | ||||
| }; | ||||
							
								
								
									
										144
									
								
								src/core/features/xapi/services/offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/core/features/xapi/services/offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreXAPIStatementDBRecord, STATEMENTS_TABLE_NAME } from './database/xapi'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline xAPI. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreXAPIOfflineProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline statements to send for a context. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with boolean: true if has offline statements, false otherwise. | ||||
|      */ | ||||
|     async contextHasStatements(contextId: number, siteId?: string): Promise<boolean> { | ||||
|         const statementsList = await this.getContextStatements(contextId, siteId); | ||||
| 
 | ||||
|         return statementsList && statementsList.length > 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete certain statements. | ||||
|      * | ||||
|      * @param id ID of the statements. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async deleteStatements(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(STATEMENTS_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all statements of a certain context. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     async deleteStatementsForContext(contextId: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(STATEMENTS_TABLE_NAME, { contextid: contextId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all offline statements. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with all the data. | ||||
|      */ | ||||
|     async getAllStatements(siteId?: string): Promise<CoreXAPIStatementDBRecord[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecords(STATEMENTS_TABLE_NAME, undefined, 'timecreated ASC'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get statements for a context. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the data. | ||||
|      */ | ||||
|     async getContextStatements(contextId: number, siteId?: string): Promise<CoreXAPIStatementDBRecord[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecords<CoreXAPIStatementDBRecord>(STATEMENTS_TABLE_NAME, { contextid: contextId }, 'timecreated ASC'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get certain statements. | ||||
|      * | ||||
|      * @param id ID of the statements. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the data. | ||||
|      */ | ||||
|     async getStatements(id: number, siteId?: string): Promise<CoreXAPIStatementDBRecord> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecord<CoreXAPIStatementDBRecord>(STATEMENTS_TABLE_NAME, { id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save statements. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param component  Component to send the statements to. | ||||
|      * @param statements Statements (JSON-encoded). | ||||
|      * @param options Options. | ||||
|      * @return Promise resolved when statements are successfully saved. | ||||
|      */ | ||||
|     async saveStatements( | ||||
|         contextId: number, | ||||
|         component: string, | ||||
|         statements: string, | ||||
|         options?: CoreXAPIOfflineSaveStatementsOptions, | ||||
|     ): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(options?.siteId); | ||||
| 
 | ||||
|         const entry: Omit<CoreXAPIStatementDBRecord, 'id'> = { | ||||
|             contextid: contextId, | ||||
|             component: component, | ||||
|             statements: statements, | ||||
|             timecreated: Date.now(), | ||||
|             courseid: options?.courseId, | ||||
|             extra: options?.extra, | ||||
|         }; | ||||
| 
 | ||||
|         await db.insertRecord(STATEMENTS_TABLE_NAME, entry); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreXAPIOffline extends makeSingleton(CoreXAPIOfflineProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * Options to pass to saveStatements function. | ||||
|  */ | ||||
| export type CoreXAPIOfflineSaveStatementsOptions = { | ||||
|     courseId?: number; // Course ID if the context is inside a course.
 | ||||
|     extra?: string; // Extra data to store.
 | ||||
|     siteId?: string; // Site ID. If not defined, current site.
 | ||||
| }; | ||||
							
								
								
									
										146
									
								
								src/core/features/xapi/services/xapi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/core/features/xapi/services/xapi.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to provide XAPI functionalities. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreXAPIProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to post XAPI statement is available. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.9 | ||||
|      */ | ||||
|     async canPostStatements(siteId?: string): Promise<boolean> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return this.canPostStatementsInSite(site); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not WS to post XAPI statement is available in a certain site. | ||||
|      * | ||||
|      * @param site Site. If not defined, current site. | ||||
|      * @return Promise resolved with true if ws is available, false otherwise. | ||||
|      * @since 3.9 | ||||
|      */ | ||||
|     canPostStatementsInSite(site?: CoreSite): boolean { | ||||
|         site = site || CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         return !!(site && site.wsAvailable('core_xapi_statement_post')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get URL for XAPI events. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param type Type (e.g. 'activity'). | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async getUrl(contextId: number, type: string, siteId?: string): Promise<string> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(site.getURL(), `xapi/${type}/${contextId}`); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Post statements. | ||||
|      * | ||||
|      * @param contextId Context ID. | ||||
|      * @param component Component. | ||||
|      * @param json JSON string to send. | ||||
|      * @param options Options. | ||||
|      * @return Promise resolved with boolean: true if response was sent to server, false if stored in device. | ||||
|      */ | ||||
|     async postStatements( | ||||
|         contextId: number, | ||||
|         component: string, | ||||
|         json: string, | ||||
|         options?: CoreXAPIPostStatementsOptions, | ||||
|     ): Promise<boolean> { | ||||
| 
 | ||||
|         options = options || {}; | ||||
|         options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a message to be synchronized later.
 | ||||
|         const storeOffline = async (): Promise<boolean> => { | ||||
|             await CoreXAPIOffline.instance.saveStatements(contextId, component, json, options); | ||||
| 
 | ||||
|             return false; | ||||
|         }; | ||||
| 
 | ||||
|         if (!CoreApp.instance.isOnline() || options.offline) { | ||||
|             // App is offline, store the action.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await this.postStatementsOnline(component, json, options.siteId); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             if (CoreUtils.instance.isWebServiceError(error)) { | ||||
|                 // The WebService has thrown an error, this means that responses cannot be submitted.
 | ||||
|                 throw error; | ||||
|             } else { | ||||
|                 // Couldn't connect to server, store it offline.
 | ||||
|                 return storeOffline(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Post statements. It will fail if offline or cannot connect. | ||||
|      * | ||||
|      * @param component Component. | ||||
|      * @param json JSON string to send. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async postStatementsOnline(component: string, json: string, siteId?: string): Promise<number[]> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const data = { | ||||
|             component: component, | ||||
|             requestjson: json, | ||||
|         }; | ||||
| 
 | ||||
|         return site.write('core_xapi_statement_post', data); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreXAPI extends makeSingleton(CoreXAPIProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * Options to pass to postStatements function. | ||||
|  */ | ||||
| export type CoreXAPIPostStatementsOptions = CoreXAPIOfflineSaveStatementsOptions & { | ||||
|     offline?: boolean; // Whether to force storing it in offline.
 | ||||
| }; | ||||
							
								
								
									
										32
									
								
								src/core/features/xapi/xapi.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/core/features/xapi/xapi.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| // (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 { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||
| import { STATEMENTS_TABLE_NAME } from './services/database/xapi'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: CORE_SITE_SCHEMAS, | ||||
|             useValue: [ | ||||
|                 STATEMENTS_TABLE_NAME, | ||||
|             ], | ||||
|             multi: true, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class CoreXAPIModule {} | ||||
| @ -70,10 +70,25 @@ export const enum CoreFileFormat { | ||||
| export class CoreFileProvider { | ||||
| 
 | ||||
|     // Formats to read a file.
 | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER; | ||||
|     /** | ||||
|      * @deprecated since 3.9.5, use CoreFileFormat directly. | ||||
|      */ | ||||
|     static readonly FORMATJSON = CoreFileFormat.FORMATJSON; | ||||
| 
 | ||||
|     // Folders.
 | ||||
| @ -460,19 +475,25 @@ export class CoreFileProvider { | ||||
|      * @param format Format to read the file. | ||||
|      * @return Promise to be resolved when the file is read. | ||||
|      */ | ||||
|     readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|     readFile( | ||||
|         path: string, | ||||
|         format?: CoreFileFormat.FORMATTEXT | CoreFileFormat.FORMATDATAURL | CoreFileFormat.FORMATBINARYSTRING, | ||||
|     ): Promise<string>; | ||||
|     readFile(path: string, format: CoreFileFormat.FORMATARRAYBUFFER): Promise<ArrayBuffer>; | ||||
|     readFile<T = unknown>(path: string, format: CoreFileFormat.FORMATJSON): Promise<T>; | ||||
|     readFile(path: string, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         // Remove basePath if it's in the path.
 | ||||
|         path = this.removeStartingSlash(path.replace(this.basePath, '')); | ||||
|         this.logger.debug('Read file ' + path + ' with format ' + format); | ||||
| 
 | ||||
|         switch (format) { | ||||
|             case CoreFileProvider.FORMATDATAURL: | ||||
|             case CoreFileFormat.FORMATDATAURL: | ||||
|                 return File.instance.readAsDataURL(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATBINARYSTRING: | ||||
|             case CoreFileFormat.FORMATBINARYSTRING: | ||||
|                 return File.instance.readAsBinaryString(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATARRAYBUFFER: | ||||
|             case CoreFileFormat.FORMATARRAYBUFFER: | ||||
|                 return File.instance.readAsArrayBuffer(this.basePath, path); | ||||
|             case CoreFileProvider.FORMATJSON: | ||||
|             case CoreFileFormat.FORMATJSON: | ||||
|                 return File.instance.readAsText(this.basePath, path).then((text) => { | ||||
|                     const parsed = CoreTextUtils.instance.parseJSON(text, null); | ||||
| 
 | ||||
| @ -494,8 +515,8 @@ export class CoreFileProvider { | ||||
|      * @param format Format to read the file. | ||||
|      * @return Promise to be resolved when the file is read. | ||||
|      */ | ||||
|     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         format = format || CoreFileProvider.FORMATTEXT; | ||||
|     readFileData(fileData: IFile, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> { | ||||
|         format = format || CoreFileFormat.FORMATTEXT; | ||||
|         this.logger.debug('Read file from file data with format ' + format); | ||||
| 
 | ||||
|         return new Promise((resolve, reject): void => { | ||||
| @ -503,7 +524,7 @@ export class CoreFileProvider { | ||||
| 
 | ||||
|             reader.onloadend = (event): void => { | ||||
|                 if (event.target?.result !== undefined && event.target.result !== null) { | ||||
|                     if (format == CoreFileProvider.FORMATJSON) { | ||||
|                     if (format == CoreFileFormat.FORMATJSON) { | ||||
|                         // Convert to object.
 | ||||
|                         const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null); | ||||
| 
 | ||||
| @ -535,13 +556,13 @@ export class CoreFileProvider { | ||||
|             }, 3000); | ||||
| 
 | ||||
|             switch (format) { | ||||
|                 case CoreFileProvider.FORMATDATAURL: | ||||
|                 case CoreFileFormat.FORMATDATAURL: | ||||
|                     reader.readAsDataURL(fileData); | ||||
|                     break; | ||||
|                 case CoreFileProvider.FORMATBINARYSTRING: | ||||
|                 case CoreFileFormat.FORMATBINARYSTRING: | ||||
|                     reader.readAsBinaryString(fileData); | ||||
|                     break; | ||||
|                 case CoreFileProvider.FORMATARRAYBUFFER: | ||||
|                 case CoreFileFormat.FORMATARRAYBUFFER: | ||||
|                     reader.readAsArrayBuffer(fileData); | ||||
|                     break; | ||||
|                 default: | ||||
|  | ||||
| @ -55,7 +55,7 @@ export class CoreUrlUtilsProvider { | ||||
|      * @param boolToNumber Whether to convert bools to 1 or 0. | ||||
|      * @return URL with params. | ||||
|      */ | ||||
|     addParamsToUrl(url: string, params?: CoreUrlParams, anchor?: string, boolToNumber?: boolean): string { | ||||
|     addParamsToUrl(url: string, params?: Record<string, unknown>, anchor?: string, boolToNumber?: boolean): string { | ||||
|         let separator = url.indexOf('?') != -1 ? '&' : '?'; | ||||
| 
 | ||||
|         for (const key in params) { | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { timeout } from 'rxjs/operators'; | ||||
| 
 | ||||
| import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreFile, CoreFileFormat } from '@services/file'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils, PromiseDefer } from '@services/utils/utils'; | ||||
| @ -855,9 +855,9 @@ export class CoreWSProvider { | ||||
|             // Use the cordova plugin.
 | ||||
|             if (url.indexOf('file://') === 0) { | ||||
|                 // We cannot load local files using the http native plugin. Use file provider instead.
 | ||||
|                 const format = options.responseType == 'json' ? CoreFileProvider.FORMATJSON : CoreFileProvider.FORMATTEXT; | ||||
| 
 | ||||
|                 const content = await CoreFile.instance.readFile(url, format); | ||||
|                 const content = options.responseType == 'json' ? | ||||
|                     await CoreFile.instance.readFile<T>(url, CoreFileFormat.FORMATJSON) : | ||||
|                     await CoreFile.instance.readFile(url, CoreFileFormat.FORMATTEXT); | ||||
| 
 | ||||
|                 return new HttpResponse<T>({ | ||||
|                     body: <T> content, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user