├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.txt ├── composer.json ├── doc └── spec_en.html ├── embed.php ├── fonts ├── h5p-core-29.eot ├── h5p-core-29.svg ├── h5p-core-29.ttf ├── h5p-core-29.woff ├── h5p-core-29.woff2 ├── h5p-core-30.eot ├── h5p-core-30.svg ├── h5p-core-30.ttf ├── h5p-core-30.woff ├── h5p-core-30.woff2 ├── h5p-hub-publish.eot ├── h5p-hub-publish.svg ├── h5p-hub-publish.ttf ├── h5p-hub-publish.woff └── open-sans │ ├── LICENSE-2.0.txt │ ├── opensans-400-600-700-v28-cyrillic-ext.woff2 │ ├── opensans-400-600-700-v28-cyrillic.woff2 │ ├── opensans-400-600-700-v28-greek-ext.woff2 │ ├── opensans-400-600-700-v28-greek.woff2 │ ├── opensans-400-600-700-v28-hebrew.woff2 │ ├── opensans-400-600-700-v28-latin-ext.woff2 │ ├── opensans-400-600-700-v28-latin.woff2 │ ├── opensans-400-600-700-v28-vietnamese.woff2 │ ├── opensans-italic-400-600-700-v28-cyrillic-ext.woff2 │ ├── opensans-italic-400-600-700-v28-cyrillic.woff2 │ ├── opensans-italic-400-600-700-v28-greek-ext.woff2 │ ├── opensans-italic-400-600-700-v28-greek.woff2 │ ├── opensans-italic-400-600-700-v28-hebrew.woff2 │ ├── opensans-italic-400-600-700-v28-latin-ext.woff2 │ ├── opensans-italic-400-600-700-v28-latin.woff2 │ └── opensans-italic-400-600-700-v28-vietnamese.woff2 ├── h5p-default-storage.class.php ├── h5p-development.class.php ├── h5p-event-base.class.php ├── h5p-file-storage.interface.php ├── h5p-metadata.class.php ├── h5p.classes.php ├── images ├── h5p.svg └── throbber.gif ├── js ├── h5p-action-bar.js ├── h5p-confirmation-dialog.js ├── h5p-content-type.js ├── h5p-content-upgrade-process.js ├── h5p-content-upgrade-worker.js ├── h5p-content-upgrade.js ├── h5p-data-view.js ├── h5p-display-options.js ├── h5p-embed.js ├── h5p-event-dispatcher.js ├── h5p-hub-registration.js ├── h5p-hub-sharing.js ├── h5p-library-details.js ├── h5p-library-list.js ├── h5p-resizer.js ├── h5p-tooltip.js ├── h5p-utils.js ├── h5p-version.js ├── h5p-x-api-event.js ├── h5p-x-api.js ├── h5p.js ├── jquery.js ├── request-queue.js └── settings │ └── h5p-disable-hub.js └── styles ├── font-open-sans.css ├── h5p-admin.css ├── h5p-confirmation-dialog.css ├── h5p-core-button.css ├── h5p-hub-registration.css ├── h5p-hub-sharing.css ├── h5p-table.css ├── h5p-tooltip.css └── h5p.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | *~ 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | # At present the only jobs to run are a php lint. 4 | # Run this against all supported versions of PHP. 5 | jobs: 6 | include: 7 | # Bionic supports PHP 7.1, 7.2, 7.3, and 7.4. 8 | # https://docs.travis-ci.com/user/reference/bionic/#php-support 9 | - php: 7.4 10 | dist: bionic 11 | - php: 7.3 12 | dist: bionic 13 | - php: 7.2 14 | dist: bionic 15 | - php: 7.1 16 | dist: bionic 17 | 18 | # Xenial was the last Travis distribution to support PHP 5.6, and 7.0. 19 | # https://docs.travis-ci.com/user/reference/xenial/#php-support 20 | - php: 7.0 21 | dist: xenial 22 | - php: 5.6 23 | dist: xenial 24 | 25 | # Trusty was the last Travis distribution to support PHP 5.4, and 5.5. 26 | # https://docs.travis-ci.com/user/languages/php/#php-54x---55x-support-is-available-on-precise-and-trusty-only 27 | - php: 5.5 28 | dist: trusty 29 | - php: 5.4 30 | dist: trusty 31 | 32 | 33 | # Precise was the last Travis distribution to support PHP 5.2, and 5.3. 34 | # https://docs.travis-ci.com/user/languages/php/#php-52x---53x-support-is-available-on-precise-only 35 | - php: 5.3 36 | dist: precise 37 | 38 | script: 39 | # Run a php lint across all PHP files. 40 | - find . -type f -name '*\.php' -print0 | xargs -0 -n1 php -l 41 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | This folder contains the general H5P library. The files within this folder are not specific to any framework. 2 | 3 | Any interaction with an LMS, CMS or other frameworks is done through interfaces. Platforms need to implement 4 | the H5PFrameworkInterface(in h5p.classes.php) and also do the following: 5 | 6 | - Provide a form for uploading H5P packages. 7 | - Place the uploaded H5P packages in a temporary directory 8 | +++ 9 | 10 | See existing implementations for details. For instance the Drupal H5P module located at drupal.org/project/h5p 11 | 12 | We will make available documentation and tutorials for creating platform integrations in the future. 13 | 14 | The H5P PHP library is GPL licensed due to GPL code being used for purifying HTML provided by authors. 15 | 16 | ## License 17 | 18 | Open Sans font is licensed under Apache license, Version 2.0 -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h5p/h5p-core", 3 | "type": "library", 4 | "description": "H5P Core functionality in PHP", 5 | "keywords": ["h5p","hvp","interactive","content","quiz"], 6 | "homepage": "https://h5p.org", 7 | "license": "GPL-3.0", 8 | "authors": [ 9 | { 10 | "name": "Svein-Tore Griff With", 11 | "email": "with@joubel.com", 12 | "homepage": "http://joubel.com", 13 | "role": "CEO" 14 | }, 15 | { 16 | "name": "Frode Petterson", 17 | "email": "frode.petterson@joubel.com", 18 | "homepage": "http://joubel.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.0.0" 24 | }, 25 | "autoload": { 26 | "files": [ 27 | "h5p.classes.php", 28 | "h5p-development.class.php", 29 | "h5p-file-storage.interface.php", 30 | "h5p-default-storage.class.php", 31 | "h5p-event-base.class.php", 32 | "h5p-metadata.class.php" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /doc/spec_en.html: -------------------------------------------------------------------------------- 1 | 

Overview

2 |

H5P is a file format for content/applications made using modern, open web technologies (HTML5). The format enables easy installation and transfer of applications/content on different CMSes, LMSes and other platforms. An H5P can be uploaded and published on a platform in mostly the same way one would publish a Flash file today. H5P files may also be updated by simply uploading a new version of the file, the same way as one would using Flash.

3 |

H5P opens for extensive reuse of code and wide flexibility regarding what may be developed as an H5P.

4 |

The system uses package files containing all necessary files and libraries for the application to function. These files are based on open formats.

5 |

Overview of package files

6 |

Package files are normal zip files, with a naming convention of <filename>.h5p to distinguish from any random zip file. This zip file then requires a specific file structure as described below.

7 |

There will be a file in JSON format named h5p.json describing the contents of the package and how the system should interpret and use it. This file contains information about title, content type, usage, copyright, licensing, version, language etc. This is described in detail below.

8 |

There shall be a folder for each included H5P library used by the package. These generic libraries may be reused by other H5P packages. As an example, a multi-choice question task may be used as a standalone block, or be included in a larger H5P package generating a game with quizzes.

9 |

Package file structure

10 |

A package contains the following elements:

11 |
    12 |
  1. A mandatory file in the root folder named h5p.json
  2. 13 |
  3. An optional image file named h5p.jpg. This is an icon or an image of the application, 512 × 512 pixels. This image may be used by the platform as a preview of the application, and could be included in OG meta tags for use with social media.
  4. 14 |
  5. One content folder, named content. This will contain the preset configuration for the application, as well as any required media files.
  6. 15 |
  7. One or more library directories named the same as the library's internal name.
  8. 16 |
17 |

h5p.json

18 |

The h5p.json file is a normal JSON text file containing a JSON object with the following predefined properties.

19 |

Mandatory properties:

20 |

Optional properties:

27 | 37 |

Eksempel på h5p.json:

38 | {
39 | "title": "Biologi-spillet",
40 | "contentType": "Game",
41 | "utilization": "Lær om biologi",
42 | "language": "nb",
43 | "author": "Amendor AS",
44 | "license": "cc-by-sa",
45 | "preloadedDependencies": [
46 | {
47 | "machineName": "H5P.Boardgame",
48 | "majorVersion": 1,
49 | "minorVersion": 0
50 | }, {
51 | "machineName": "H5P.QuestionSet",
52 | "majorVersion": 1,
53 | "minorVersion": 0
54 | }, {
55 | "machineName": "H5P.MultiChoice",
56 | "majorVersion": 1, "minorVersion": 0
57 | }, {
58 | "machineName": "EmbeddedJS",
59 | "majorVersion": 1,
60 | "minorVersion": 0
61 | } ],
62 | "embedTypes": ["div", "iframe"],
63 | "w": 635,
64 | "h": 500
65 | }
66 |

The content folder

67 |

Contains all the content for the package and its libraries. There shall be no content inside the library folders. The content folder shall contain a file named content.json, containing the JSON object that will be passed to the initializer for the main package library.

68 | 69 |

Content required by libraries invoked from the main package library will get their contents passed from the main library. The JSON for this will be found within the main content.json for the package, and passed during initialization.

70 | 71 |

Library folders

72 | 73 |

A library folder contains all logic, stylesheets and graphics that will be common for all instances of a library. There shall be no content or interface text directly in these folders. All text displayed to the end user shall be passed as part of the library configuration. This make the libraries language independent.

74 | 75 |

The root of a library folder shall contain a file name library.json formatted similar to the package's hp5.json, but with a few differences. The library shall also have one or more images in the root folder, named library.jpg, library1.jpg etc. Image sizes 512px × 512px, and will be used in the H5P editor tool.

76 | 77 |

Libraries are not allowed to modify the document tree in ways that will have consequences for the web site or will be noticeable by the user without the library explicitly being initialized from the main package library or another invoked library.

78 | 79 |

The library shall always include a JavaScript object function named the same as the defined library machineName (defined in library.json and used as the library folder name). This object will be instantiated with the library options as parameter. The resulting object must contain a function attach(target) that will be called after instantiation to attach the library DOM to the main DOM inside target

80 | 81 |

Example

82 |

A library called H5P.multichoice would typically be instantiated and attached to the page like this:

83 | var multichoice = new H5P.multichoice(contentFromJson, contentId);
84 | multichoice.attach($multichoiceContainer);
85 | 86 |

library.json

87 |

Mandatory properties:

88 | 96 |

Optional properties:

97 | 108 |

Eksempel på library.json:

109 | {
110 | "title": "Boardgame",
111 | "description": "The user is presented with a board with several hotspots. By clicking a hotspot he invokes a mini-game.",
112 | "majorVersion": 1,
113 | "minorVersion": 0,
114 | "patchVersion": 6,
115 | "runnable": 1,
116 | "machineName": "H5P.Boardgame",
117 | "author": "Amendor AS",
118 | "license": "cc-by-sa",
119 | "preloadedDependencies": [
120 | {
121 | "machineName": "EmbeddedJS",
122 | "majorVersion": 1,
123 | "minorVersion": 0
124 | }, {
125 | "machineName": "H5P.MultiChoice",
126 | "majorVersion": 1,
127 | "minorVersion": 0
128 | }, {
129 | "machineName": "H5P.QuestionSet",
130 | "majorVersion": 1,
131 | "minorVersion": 0
132 | } ],
133 | "preloadedCss": [ {"path": "css/boardgame.css"} ],
134 | "preloadedJs": [ {"path": "js/boardgame.js"} ],
135 | "w": 635,
136 | "h": 500 }
137 | 138 |

Allowed file types

139 |

Files that require server side execution or that cannot be regarded an open standard shall not be used. Allowed file types: js, json, png, jpg, gif, svg, css, mp3, wav (audio: PCM), m4a (audio: AAC), mp4 (video: H.264, audio: AAC/MP3), ogg (video: Theora, audio: Vorbis) and webm (video VP8, audio: Vorbis). Administrators of web sites implementing H5P may open for accepting further formats. HTML files shall not be used. HTML for each library shall be inserted from the library scripts to ease code reuse. (By avoiding content being defined in said HTML).

140 |

API functions

141 |

The following JavaScript functions are available through h5p:

142 | 152 |

I tillegg er følgende api funksjoner tilgjengelig via ndla:

153 | 156 |

Best practices

157 |

H5P is a very open standard. This is positive for flexibility. Most content may be produces as H5P. But this also allows for bad code, security weaknesses, code that may be difficult to reuse. Therefore the following best practices should be followed to get the most from H5P:

158 | 169 | -------------------------------------------------------------------------------- /embed.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <?php print $content['title']; ?> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /fonts/h5p-core-29.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-29.eot -------------------------------------------------------------------------------- /fonts/h5p-core-29.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-29.ttf -------------------------------------------------------------------------------- /fonts/h5p-core-29.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-29.woff -------------------------------------------------------------------------------- /fonts/h5p-core-29.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-29.woff2 -------------------------------------------------------------------------------- /fonts/h5p-core-30.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-30.eot -------------------------------------------------------------------------------- /fonts/h5p-core-30.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-30.ttf -------------------------------------------------------------------------------- /fonts/h5p-core-30.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-30.woff -------------------------------------------------------------------------------- /fonts/h5p-core-30.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-core-30.woff2 -------------------------------------------------------------------------------- /fonts/h5p-hub-publish.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-hub-publish.eot -------------------------------------------------------------------------------- /fonts/h5p-hub-publish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | { 8 | "fontFamily": "h5p-hub", 9 | "description": "Font generated by IcoMoon.", 10 | "majorVersion": 1, 11 | "minorVersion": 3, 12 | "version": "Version 1.3", 13 | "fontId": "h5p-hub", 14 | "psName": "h5p-hub", 15 | "subFamily": "Regular", 16 | "fullName": "h5p-hub" 17 | } 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /fonts/h5p-hub-publish.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-hub-publish.ttf -------------------------------------------------------------------------------- /fonts/h5p-hub-publish.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/h5p-hub-publish.woff -------------------------------------------------------------------------------- /fonts/open-sans/LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-cyrillic.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-greek-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-greek.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-hebrew.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-hebrew.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-latin-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-latin.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-400-600-700-v28-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-400-600-700-v28-vietnamese.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-cyrillic.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-greek-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-greek.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-hebrew.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-hebrew.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-latin-ext.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-latin.woff2 -------------------------------------------------------------------------------- /fonts/open-sans/opensans-italic-400-600-700-v28-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/fonts/open-sans/opensans-italic-400-600-700-v28-vietnamese.woff2 -------------------------------------------------------------------------------- /h5p-development.class.php: -------------------------------------------------------------------------------- 1 | h5pF = $H5PFramework; 26 | $this->language = $language; 27 | $this->filesPath = $filesPath; 28 | if ($libraries !== NULL) { 29 | $this->libraries = $libraries; 30 | } 31 | else { 32 | $this->findLibraries($filesPath . '/development'); 33 | } 34 | } 35 | 36 | /** 37 | * Get contents of file. 38 | * 39 | * @param string $file File path. 40 | * @return mixed String on success or NULL on failure. 41 | */ 42 | private function getFileContents($file) { 43 | if (file_exists($file) === FALSE) { 44 | return NULL; 45 | } 46 | 47 | $contents = file_get_contents($file); 48 | if ($contents === FALSE) { 49 | return NULL; 50 | } 51 | 52 | return $contents; 53 | } 54 | 55 | /** 56 | * Scans development directory and find all libraries. 57 | * 58 | * @param string $path Libraries development folder 59 | */ 60 | private function findLibraries($path) { 61 | $this->libraries = array(); 62 | 63 | if (is_dir($path) === FALSE) { 64 | return; 65 | } 66 | 67 | $contents = scandir($path); 68 | 69 | for ($i = 0, $s = count($contents); $i < $s; $i++) { 70 | if ($contents[$i][0] === '.') { 71 | continue; // Skip hidden stuff. 72 | } 73 | 74 | $libraryPath = $path . '/' . $contents[$i]; 75 | $libraryJSON = $this->getFileContents($libraryPath . '/library.json'); 76 | if ($libraryJSON === NULL) { 77 | continue; // No JSON file, skip. 78 | } 79 | 80 | $library = json_decode($libraryJSON, TRUE); 81 | if ($library === NULL) { 82 | continue; // Invalid JSON. 83 | } 84 | 85 | // TODO: Validate props? Not really needed, is it? this is a dev site. 86 | 87 | $library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); 88 | 89 | // Convert metadataSettings values to boolean & json_encode it before saving 90 | $library['metadataSettings'] = isset($library['metadataSettings']) ? 91 | H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) : 92 | NULL; 93 | 94 | // Save/update library. 95 | $this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE); 96 | 97 | // Need to decode it again, since it is served from here. 98 | $library['metadataSettings'] = isset($library['metadataSettings']) 99 | ? json_decode($library['metadataSettings']) 100 | : NULL; 101 | 102 | $library['path'] = 'development/' . $contents[$i]; 103 | $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; 104 | } 105 | 106 | // TODO: Should we remove libraries without files? Not really needed, but must be cleaned up some time, right? 107 | 108 | // Go trough libraries and insert dependencies. Missing deps. will just be ignored and not available. (I guess?!) 109 | $this->h5pF->lockDependencyStorage(); 110 | foreach ($this->libraries as $library) { 111 | $this->h5pF->deleteLibraryDependencies($library['libraryId']); 112 | // This isn't optimal, but without it we would get duplicate warnings. 113 | // TODO: You might get PDOExceptions if two or more requests does this at the same time!! 114 | $types = array('preloaded', 'dynamic', 'editor'); 115 | foreach ($types as $type) { 116 | if (isset($library[$type . 'Dependencies'])) { 117 | $this->h5pF->saveLibraryDependencies($library['libraryId'], $library[$type . 'Dependencies'], $type); 118 | } 119 | } 120 | } 121 | $this->h5pF->unlockDependencyStorage(); 122 | // TODO: Deps must be inserted into h5p_nodes_libraries as well... ? But only if they are used?! 123 | } 124 | 125 | /** 126 | * @return array Libraries in development folder. 127 | */ 128 | public function getLibraries() { 129 | return $this->libraries; 130 | } 131 | 132 | /** 133 | * Get library 134 | * 135 | * @param string $name of the library. 136 | * @param int $majorVersion of the library. 137 | * @param int $minorVersion of the library. 138 | * @return array library. 139 | */ 140 | public function getLibrary($name, $majorVersion, $minorVersion) { 141 | $library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion); 142 | return isset($this->libraries[$library]) === TRUE ? $this->libraries[$library] : NULL; 143 | } 144 | 145 | /** 146 | * Get semantics for the given library. 147 | * 148 | * @param string $name of the library. 149 | * @param int $majorVersion of the library. 150 | * @param int $minorVersion of the library. 151 | * @return string Semantics 152 | */ 153 | public function getSemantics($name, $majorVersion, $minorVersion) { 154 | $library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion); 155 | if (isset($this->libraries[$library]) === FALSE) { 156 | return NULL; 157 | } 158 | return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/semantics.json'); 159 | } 160 | 161 | /** 162 | * Get translations for the given library. 163 | * 164 | * @param string $name of the library. 165 | * @param int $majorVersion of the library. 166 | * @param int $minorVersion of the library. 167 | * @param $language 168 | * @return string Translation 169 | */ 170 | public function getLanguage($name, $majorVersion, $minorVersion, $language) { 171 | $library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion); 172 | 173 | if (isset($this->libraries[$library]) === FALSE) { 174 | return NULL; 175 | } 176 | 177 | return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/language/' . $language . '.json'); 178 | } 179 | 180 | /** 181 | * Writes library as string on the form "name majorVersion.minorVersion" 182 | * 183 | * @param string $name Machine readable library name 184 | * @param integer $majorVersion 185 | * @param $minorVersion 186 | * @return string Library identifier. 187 | */ 188 | public static function libraryToString($name, $majorVersion, $minorVersion) { 189 | return $name . ' ' . $majorVersion . '.' . $minorVersion; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /h5p-event-base.class.php: -------------------------------------------------------------------------------- 1 | – content view 28 | * embed – viewed through embed code 29 | * shortcode – viewed through internal shortcode 30 | * edit – opened in editor 31 | * delete – deleted 32 | * create – created through editor 33 | * create upload – created through upload 34 | * update – updated through editor 35 | * update upload – updated through upload 36 | * upgrade – upgraded 37 | * 38 | * results, – view own results 39 | * content – view results for content 40 | * set – new results inserted or updated 41 | * 42 | * settings, – settings page loaded 43 | * 44 | * library, – loaded in editor 45 | * create – new library installed 46 | * update – old library updated 47 | * 48 | * @param string $type 49 | * Name of event type 50 | * @param string $sub_type 51 | * Name of event sub type 52 | * @param string $content_id 53 | * Identifier for content affected by the event 54 | * @param string $content_title 55 | * Content title (makes it easier to know which content was deleted etc.) 56 | * @param string $library_name 57 | * Name of the library affected by the event 58 | * @param string $library_version 59 | * Library version 60 | */ 61 | function __construct($type, $sub_type = NULL, $content_id = NULL, $content_title = NULL, $library_name = NULL, $library_version = NULL) { 62 | $this->type = $type; 63 | $this->sub_type = $sub_type; 64 | $this->content_id = $content_id; 65 | $this->content_title = $content_title; 66 | $this->library_name = $library_name; 67 | $this->library_version = $library_version; 68 | $this->time = time(); 69 | 70 | if (self::validLogLevel($type, $sub_type)) { 71 | $this->save(); 72 | } 73 | if (self::validStats($type, $sub_type)) { 74 | $this->saveStats(); 75 | } 76 | } 77 | 78 | /** 79 | * Determines if the event type should be saved/logged. 80 | * 81 | * @param string $type 82 | * Name of event type 83 | * @param string $sub_type 84 | * Name of event sub type 85 | * @return boolean 86 | */ 87 | private static function validLogLevel($type, $sub_type) { 88 | switch (self::$log_level) { 89 | default: 90 | case self::LOG_NONE: 91 | return FALSE; 92 | case self::LOG_ALL: 93 | return TRUE; // Log everything 94 | case self::LOG_ACTIONS: 95 | if (self::isAction($type, $sub_type)) { 96 | return TRUE; // Log actions 97 | } 98 | return FALSE; 99 | } 100 | } 101 | 102 | /** 103 | * Check if the event should be included in the statistics counter. 104 | * 105 | * @param string $type 106 | * Name of event type 107 | * @param string $sub_type 108 | * Name of event sub type 109 | * @return boolean 110 | */ 111 | private static function validStats($type, $sub_type) { 112 | if ( ($type === 'content' && $sub_type === 'shortcode insert') || // Count number of shortcode inserts 113 | ($type === 'library' && $sub_type === NULL) || // Count number of times library is loaded in editor 114 | ($type === 'results' && $sub_type === 'content') ) { // Count number of times results page has been opened 115 | return TRUE; 116 | } 117 | elseif (self::isAction($type, $sub_type)) { // Count all actions 118 | return TRUE; 119 | } 120 | return FALSE; 121 | } 122 | 123 | /** 124 | * Check if event type is an action. 125 | * 126 | * @param string $type 127 | * Name of event type 128 | * @param string $sub_type 129 | * Name of event sub type 130 | * @return boolean 131 | */ 132 | private static function isAction($type, $sub_type) { 133 | if ( ($type === 'content' && in_array($sub_type, array('create', 'create upload', 'update', 'update upload', 'upgrade', 'delete'))) || 134 | ($type === 'library' && in_array($sub_type, array('create', 'update'))) ) { 135 | return TRUE; // Log actions 136 | } 137 | return FALSE; 138 | } 139 | 140 | /** 141 | * A helper which makes it easier for systems to save the data. 142 | * Add all relevant properties to a assoc. array. 143 | * There are no NULL values. Empty string or 0 is used instead. 144 | * Used by both Drupal and WordPress. 145 | * 146 | * @return array with keyed values 147 | */ 148 | protected function getDataArray() { 149 | return array( 150 | 'created_at' => $this->time, 151 | 'type' => $this->type, 152 | 'sub_type' => empty($this->sub_type) ? '' : $this->sub_type, 153 | 'content_id' => empty($this->content_id) ? 0 : $this->content_id, 154 | 'content_title' => empty($this->content_title) ? '' : $this->content_title, 155 | 'library_name' => empty($this->library_name) ? '' : $this->library_name, 156 | 'library_version' => empty($this->library_version) ? '' : $this->library_version 157 | ); 158 | } 159 | 160 | /** 161 | * A helper which makes it easier for systems to save the data. 162 | * Used in WordPress. 163 | * 164 | * @return array with strings 165 | */ 166 | protected function getFormatArray() { 167 | return array( 168 | '%d', 169 | '%s', 170 | '%s', 171 | '%d', 172 | '%s', 173 | '%s', 174 | '%s' 175 | ); 176 | } 177 | 178 | /** 179 | * Stores the event data in the database. 180 | * 181 | * Must be overridden by plugin. 182 | */ 183 | abstract protected function save(); 184 | 185 | /** 186 | * Add current event data to statistics counter. 187 | * 188 | * Must be overridden by plugin. 189 | */ 190 | abstract protected function saveStats(); 191 | } 192 | -------------------------------------------------------------------------------- /h5p-file-storage.interface.php: -------------------------------------------------------------------------------- 1 | array( 9 | 'type' => 'text', 10 | 'maxLength' => 255 11 | ), 12 | 'a11yTitle' => array( 13 | 'type' => 'text', 14 | 'maxLength' => 255, 15 | ), 16 | 'authors' => array( 17 | 'type' => 'json' 18 | ), 19 | 'changes' => array( 20 | 'type' => 'json' 21 | ), 22 | 'source' => array( 23 | 'type' => 'text', 24 | 'maxLength' => 255 25 | ), 26 | 'license' => array( 27 | 'type' => 'text', 28 | 'maxLength' => 32 29 | ), 30 | 'licenseVersion' => array( 31 | 'type' => 'text', 32 | 'maxLength' => 10 33 | ), 34 | 'licenseExtras' => array( 35 | 'type' => 'text', 36 | 'maxLength' => 5000 37 | ), 38 | 'authorComments' => array( 39 | 'type' => 'text', 40 | 'maxLength' => 5000 41 | ), 42 | 'yearFrom' => array( 43 | 'type' => 'int' 44 | ), 45 | 'yearTo' => array( 46 | 'type' => 'int' 47 | ), 48 | 'defaultLanguage' => array( 49 | 'type' => 'text', 50 | 'maxLength' => 32, 51 | ) 52 | ); 53 | 54 | /** 55 | * JSON encode metadata 56 | * 57 | * @param object $content 58 | * @return string 59 | */ 60 | public static function toJSON($content) { 61 | // Note: deliberatly creating JSON string "manually" to improve performance 62 | return 63 | '{"title":' . (isset($content->title) ? json_encode($content->title) : 'null') . 64 | ',"a11yTitle":' . (isset($content->a11y_title) ? $content->a11y_title : 'null') . 65 | ',"authors":' . (isset($content->authors) ? $content->authors : 'null') . 66 | ',"source":' . (isset($content->source) ? '"' . $content->source . '"' : 'null') . 67 | ',"license":' . (isset($content->license) ? '"' . $content->license . '"' : 'null') . 68 | ',"licenseVersion":' . (isset($content->license_version) ? '"' . $content->license_version . '"' : 'null') . 69 | ',"licenseExtras":' . (isset($content->license_extras) ? json_encode($content->license_extras) : 'null') . 70 | ',"yearFrom":' . (isset($content->year_from) ? $content->year_from : 'null') . 71 | ',"yearTo":' . (isset($content->year_to) ? $content->year_to : 'null') . 72 | ',"changes":' . (isset($content->changes) ? $content->changes : 'null') . 73 | ',"defaultLanguage":' . (isset($content->default_language) ? '"' . $content->default_language . '"' : 'null') . 74 | ',"authorComments":' . (isset($content->author_comments) ? json_encode($content->author_comments) : 'null') . '}'; 75 | } 76 | 77 | /** 78 | * Make the metadata into an associative array keyed by the property names 79 | * @param mixed $metadata Array or object containing metadata 80 | * @param bool $include_title 81 | * @param bool $include_missing For metadata fields not being set, skip 'em. 82 | * Relevant for content upgrade 83 | * @param array $types 84 | * @return array 85 | */ 86 | public static function toDBArray($metadata, $include_title = true, $include_missing = true, &$types = array()) { 87 | $fields = array(); 88 | 89 | if (!is_array($metadata)) { 90 | $metadata = (array) $metadata; 91 | } 92 | 93 | foreach (self::$fields as $key => $config) { 94 | 95 | // Ignore title? 96 | if ($key === 'title' && !$include_title) { 97 | continue; 98 | } 99 | 100 | $exists = array_key_exists($key, $metadata); 101 | 102 | // Don't include missing fields 103 | if (!$include_missing && !$exists) { 104 | continue; 105 | } 106 | 107 | $value = $exists ? $metadata[$key] : null; 108 | 109 | // lowerCamelCase to snake_case 110 | $db_field_name = strtolower(preg_replace('/(? $config['maxLength']) { 115 | $value = mb_substr($value, 0, $config['maxLength']); 116 | } 117 | $types[] = '%s'; 118 | break; 119 | 120 | case 'int': 121 | $value = ($value !== null) ? intval($value) : null; 122 | $types[] = '%d'; 123 | break; 124 | 125 | case 'json': 126 | $value = ($value !== null) ? json_encode($value) : null; 127 | $types[] = '%s'; 128 | break; 129 | } 130 | 131 | $fields[$db_field_name] = $value; 132 | } 133 | 134 | return $fields; 135 | } 136 | 137 | /** 138 | * The metadataSettings field in libraryJson uses 1 for true and 0 for false. 139 | * Here we are converting these to booleans, and also doing JSON encoding. 140 | * This is invoked before the library data is beeing inserted/updated to DB. 141 | * 142 | * @param array $metadataSettings 143 | * @return string 144 | */ 145 | public static function boolifyAndEncodeSettings($metadataSettings) { 146 | // Convert metadataSettings values to boolean 147 | if (isset($metadataSettings['disable'])) { 148 | $metadataSettings['disable'] = $metadataSettings['disable'] === 1; 149 | } 150 | if (isset($metadataSettings['disableExtraTitleField'])) { 151 | $metadataSettings['disableExtraTitleField'] = $metadataSettings['disableExtraTitleField'] === 1; 152 | } 153 | 154 | return json_encode($metadataSettings); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /images/h5p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h5p/h5p-php-library/b5f527e140c17da2792283d369f621e9b3f969ff/images/throbber.gif -------------------------------------------------------------------------------- /js/h5p-action-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @augments H5P.EventDispatcher 4 | * @param {Object} displayOptions 5 | * @param {boolean} displayOptions.export Triggers the display of the 'Download' button 6 | * @param {boolean} displayOptions.copyright Triggers the display of the 'Copyright' button 7 | * @param {boolean} displayOptions.embed Triggers the display of the 'Embed' button 8 | * @param {boolean} displayOptions.icon Triggers the display of the 'H5P icon' link 9 | */ 10 | H5P.ActionBar = (function ($, EventDispatcher) { 11 | "use strict"; 12 | 13 | function ActionBar(displayOptions) { 14 | EventDispatcher.call(this); 15 | 16 | /** @alias H5P.ActionBar# */ 17 | var self = this; 18 | 19 | var hasActions = false; 20 | 21 | // Create action bar 22 | var $actions = H5P.jQuery('
    '); 23 | 24 | /** 25 | * Helper for creating action bar buttons. 26 | * 27 | * @private 28 | * @param {string} type 29 | * @param {string} customClass Instead of type class 30 | */ 31 | var addActionButton = function (type, customClass) { 32 | /** 33 | * Handles selection of action 34 | */ 35 | var handler = function () { 36 | self.trigger(type); 37 | }; 38 | 39 | const $actionList = H5P.jQuery('
  • ', { 40 | 'class': 'h5p-button h5p-noselect h5p-' + (customClass ? customClass : type), 41 | appendTo: $actions 42 | }); 43 | 44 | const $actionButton = H5P.jQuery(''); 98 | H5PLibraryDetails.$next = $(''); 99 | 100 | H5PLibraryDetails.$previous.on('click', function () { 101 | if (H5PLibraryDetails.$previous.hasClass('disabled')) { 102 | return; 103 | } 104 | 105 | H5PLibraryDetails.currentPage--; 106 | H5PLibraryDetails.updatePager(); 107 | H5PLibraryDetails.createContentTable(); 108 | }); 109 | 110 | H5PLibraryDetails.$next.on('click', function () { 111 | if (H5PLibraryDetails.$next.hasClass('disabled')) { 112 | return; 113 | } 114 | 115 | H5PLibraryDetails.currentPage++; 116 | H5PLibraryDetails.updatePager(); 117 | H5PLibraryDetails.createContentTable(); 118 | }); 119 | 120 | // This is the Page x of y widget: 121 | H5PLibraryDetails.$pagerInfo = $(''); 122 | 123 | H5PLibraryDetails.$pager = $('
    ').append(H5PLibraryDetails.$previous, H5PLibraryDetails.$pagerInfo, H5PLibraryDetails.$next); 124 | H5PLibraryDetails.$content.append(H5PLibraryDetails.$pager); 125 | 126 | H5PLibraryDetails.$pagerInfo.on('click', function () { 127 | var width = H5PLibraryDetails.$pagerInfo.innerWidth(); 128 | H5PLibraryDetails.$pagerInfo.hide(); 129 | 130 | // User has updated the pageNumber 131 | var pageNumerUpdated = function () { 132 | var newPageNum = $gotoInput.val()-1; 133 | var intRegex = /^\d+$/; 134 | 135 | $goto.remove(); 136 | H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); 137 | 138 | // Check if input value is valid, and that it has actually changed 139 | if (!(intRegex.test(newPageNum) && newPageNum >= 0 && newPageNum < H5PLibraryDetails.getNumPages() && newPageNum != H5PLibraryDetails.currentPage)) { 140 | return; 141 | } 142 | 143 | H5PLibraryDetails.currentPage = newPageNum; 144 | H5PLibraryDetails.updatePager(); 145 | H5PLibraryDetails.createContentTable(); 146 | }; 147 | 148 | // We create an input box where the user may type in the page number 149 | // he wants to be displayed. 150 | // Reson for doing this is when user has ten-thousands of elements in list, 151 | // this is the easiest way of getting to a specified page 152 | var $gotoInput = $('', { 153 | type: 'number', 154 | min : 1, 155 | max: H5PLibraryDetails.getNumPages(), 156 | on: { 157 | // Listen to blur, and the enter-key: 158 | 'blur': pageNumerUpdated, 159 | 'keyup': function (event) { 160 | if (event.keyCode === 13) { 161 | pageNumerUpdated(); 162 | } 163 | } 164 | } 165 | }).css({width: width}); 166 | var $goto = $('', { 167 | 'class': 'h5p-pager-goto' 168 | }).css({width: width}).append($gotoInput).insertAfter(H5PLibraryDetails.$pagerInfo); 169 | 170 | $gotoInput.focus(); 171 | }); 172 | 173 | H5PLibraryDetails.updatePager(); 174 | }; 175 | 176 | /** 177 | * Calculates number of pages 178 | */ 179 | H5PLibraryDetails.getNumPages = function () { 180 | return Math.ceil(H5PLibraryDetails.currentContent.length / H5PLibraryDetails.PAGER_SIZE); 181 | }; 182 | 183 | /** 184 | * Update the pager text, and enables/disables the next and previous buttons as needed 185 | */ 186 | H5PLibraryDetails.updatePager = function () { 187 | H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); 188 | 189 | if (H5PLibraryDetails.getNumPages() > 0) { 190 | var message = H5PUtils.translateReplace(H5PLibraryDetails.library.translations.pageXOfY, { 191 | '$x': (H5PLibraryDetails.currentPage+1), 192 | '$y': H5PLibraryDetails.getNumPages() 193 | }); 194 | H5PLibraryDetails.$pagerInfo.html(message); 195 | } 196 | else { 197 | H5PLibraryDetails.$pagerInfo.html(''); 198 | } 199 | 200 | H5PLibraryDetails.$previous.toggleClass('disabled', H5PLibraryDetails.currentPage <= 0); 201 | H5PLibraryDetails.$next.toggleClass('disabled', H5PLibraryDetails.currentContent.length < (H5PLibraryDetails.currentPage+1)*H5PLibraryDetails.PAGER_SIZE); 202 | }; 203 | 204 | /** 205 | * Creates the search element 206 | */ 207 | H5PLibraryDetails.createSearchElement = function () { 208 | 209 | H5PLibraryDetails.$search = $(''); 210 | 211 | var performSeach = function () { 212 | var searchString = $('.h5p-content-search > input').val(); 213 | 214 | // If search string same as previous, just do nothing 215 | if (H5PLibraryDetails.currentFilter === searchString) { 216 | return; 217 | } 218 | 219 | if (searchString.trim().length === 0) { 220 | // If empty search, use the complete list 221 | H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; 222 | } 223 | else if (H5PLibraryDetails.filterCache[searchString]) { 224 | // If search is cached, no need to filter 225 | H5PLibraryDetails.currentContent = H5PLibraryDetails.filterCache[searchString]; 226 | } 227 | else { 228 | var listToFilter = H5PLibraryDetails.library.content; 229 | 230 | // Check if we can filter the already filtered results (for performance) 231 | if (searchString.length > 1 && H5PLibraryDetails.currentFilter === searchString.substr(0, H5PLibraryDetails.currentFilter.length)) { 232 | listToFilter = H5PLibraryDetails.currentContent; 233 | } 234 | H5PLibraryDetails.currentContent = $.grep(listToFilter, function (content) { 235 | return content.title && content.title.match(new RegExp(searchString, 'i')); 236 | }); 237 | } 238 | 239 | H5PLibraryDetails.currentFilter = searchString; 240 | // Cache the current result 241 | H5PLibraryDetails.filterCache[searchString] = H5PLibraryDetails.currentContent; 242 | H5PLibraryDetails.currentPage = 0; 243 | H5PLibraryDetails.createContentTable(); 244 | 245 | // Display search results: 246 | if (H5PLibraryDetails.$searchResults) { 247 | H5PLibraryDetails.$searchResults.remove(); 248 | } 249 | if (searchString.trim().length > 0) { 250 | H5PLibraryDetails.$searchResults = $('' + H5PLibraryDetails.currentContent.length + ' hits on ' + H5PLibraryDetails.currentFilter + ''); 251 | H5PLibraryDetails.$search.append(H5PLibraryDetails.$searchResults); 252 | } 253 | H5PLibraryDetails.updatePager(); 254 | }; 255 | 256 | var inputTimer; 257 | $('input', H5PLibraryDetails.$search).on('change keypress paste input', function () { 258 | // Here we start the filtering 259 | // We wait at least 500 ms after last input to perform search 260 | if (inputTimer) { 261 | clearTimeout(inputTimer); 262 | } 263 | 264 | inputTimer = setTimeout( function () { 265 | performSeach(); 266 | }, 500); 267 | }); 268 | 269 | H5PLibraryDetails.$content.append(H5PLibraryDetails.$search); 270 | }; 271 | 272 | /** 273 | * Creates the page size selector 274 | */ 275 | H5PLibraryDetails.createPageSizeSelector = function () { 276 | H5PLibraryDetails.$search.append('
    ' + H5PLibraryDetails.library.translations.pageSizeSelectorLabel + ':102050100200
    '); 277 | 278 | // Listen to clicks on the page size selector: 279 | $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).on('click', function () { 280 | H5PLibraryDetails.PAGER_SIZE = $(this).data('page-size'); 281 | $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).removeClass('selected'); 282 | $(this).addClass('selected'); 283 | H5PLibraryDetails.currentPage = 0; 284 | H5PLibraryDetails.createContentTable(); 285 | H5PLibraryDetails.updatePager(); 286 | }); 287 | }; 288 | 289 | // Initialize me: 290 | $(document).ready(function () { 291 | if (!H5PLibraryDetails.initialized) { 292 | H5PLibraryDetails.initialized = true; 293 | H5PLibraryDetails.init(); 294 | } 295 | }); 296 | 297 | })(H5P.jQuery); 298 | -------------------------------------------------------------------------------- /js/h5p-library-list.js: -------------------------------------------------------------------------------- 1 | /* global H5PAdminIntegration H5PUtils */ 2 | var H5PLibraryList = H5PLibraryList || {}; 3 | 4 | (function ($) { 5 | 6 | /** 7 | * Initializing 8 | */ 9 | H5PLibraryList.init = function () { 10 | var $adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector).html(''); 11 | 12 | var libraryList = H5PAdminIntegration.libraryList; 13 | if (libraryList.notCached) { 14 | $adminContainer.append(H5PUtils.getRebuildCache(libraryList.notCached)); 15 | } 16 | 17 | // Create library list 18 | $adminContainer.append(H5PLibraryList.createLibraryList(H5PAdminIntegration.libraryList)); 19 | }; 20 | 21 | /** 22 | * Create the library list 23 | * 24 | * @param {object} libraries List of libraries and headers 25 | */ 26 | H5PLibraryList.createLibraryList = function (libraries) { 27 | var t = H5PAdminIntegration.l10n; 28 | if (libraries.listData === undefined || libraries.listData.length === 0) { 29 | return $('
    ' + t.NA + '
    '); 30 | } 31 | 32 | // Create table 33 | var $table = H5PUtils.createTable(libraries.listHeaders); 34 | $table.addClass('libraries'); 35 | 36 | // Add libraries 37 | $.each (libraries.listData, function (index, library) { 38 | var $libraryRow = H5PUtils.createTableRow([ 39 | library.title, 40 | '', 41 | { 42 | text: library.numContent, 43 | class: 'h5p-admin-center' 44 | }, 45 | { 46 | text: library.numContentDependencies, 47 | class: 'h5p-admin-center' 48 | }, 49 | { 50 | text: library.numLibraryDependencies, 51 | class: 'h5p-admin-center' 52 | }, 53 | '
    ' + 54 | '' + 55 | (library.detailsUrl ? '' : '') + 56 | (library.deleteUrl ? '' : '') + 57 | '
    ' 58 | ]); 59 | 60 | H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted); 61 | 62 | var hasContent = !(library.numContent === '' || library.numContent === 0); 63 | if (library.upgradeUrl === null) { 64 | $('.h5p-admin-upgrade-library', $libraryRow).remove(); 65 | } 66 | else if (library.upgradeUrl === false || !hasContent) { 67 | $('.h5p-admin-upgrade-library', $libraryRow).attr('disabled', true); 68 | } 69 | else { 70 | $('.h5p-admin-upgrade-library', $libraryRow).attr('title', t.upgradeLibrary).click(function () { 71 | window.location.href = library.upgradeUrl; 72 | }); 73 | } 74 | 75 | // Open details view when clicked 76 | $('.h5p-admin-view-library', $libraryRow).on('click', function () { 77 | window.location.href = library.detailsUrl; 78 | }); 79 | 80 | var $deleteButton = $('.h5p-admin-delete-library', $libraryRow); 81 | if (libraries.notCached !== undefined || 82 | hasContent || 83 | (library.numContentDependencies !== '' && 84 | library.numContentDependencies !== 0) || 85 | (library.numLibraryDependencies !== '' && 86 | library.numLibraryDependencies !== 0)) { 87 | // Disabled delete if content. 88 | $deleteButton.attr('disabled', true); 89 | } 90 | else { 91 | // Go to delete page om click. 92 | $deleteButton.attr('title', t.deleteLibrary).on('click', function () { 93 | window.location.href = library.deleteUrl; 94 | }); 95 | } 96 | 97 | $table.append($libraryRow); 98 | }); 99 | 100 | return $table; 101 | }; 102 | 103 | H5PLibraryList.addRestricted = function ($checkbox, url, selected) { 104 | if (selected === null) { 105 | $checkbox.remove(); 106 | } 107 | else { 108 | $checkbox.change(function () { 109 | $checkbox.attr('disabled', true); 110 | 111 | $.ajax({ 112 | dataType: 'json', 113 | url: url, 114 | cache: false 115 | }).fail(function () { 116 | $checkbox.attr('disabled', false); 117 | 118 | // Reset 119 | $checkbox.attr('checked', !$checkbox.is(':checked')); 120 | }).done(function (result) { 121 | url = result.url; 122 | $checkbox.attr('disabled', false); 123 | }); 124 | }); 125 | 126 | if (selected) { 127 | $checkbox.attr('checked', true); 128 | } 129 | } 130 | }; 131 | 132 | // Initialize me: 133 | $(document).ready(function () { 134 | if (!H5PLibraryList.initialized) { 135 | H5PLibraryList.initialized = true; 136 | H5PLibraryList.init(); 137 | } 138 | }); 139 | 140 | })(H5P.jQuery); 141 | -------------------------------------------------------------------------------- /js/h5p-resizer.js: -------------------------------------------------------------------------------- 1 | // H5P iframe Resizer 2 | (function () { 3 | if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { 4 | return; // Not supported 5 | } 6 | window.h5pResizerInitialized = true; 7 | 8 | // Map actions to handlers 9 | var actionHandlers = {}; 10 | 11 | /** 12 | * Prepare iframe resize. 13 | * 14 | * @private 15 | * @param {Object} iframe Element 16 | * @param {Object} data Payload 17 | * @param {Function} respond Send a response to the iframe 18 | */ 19 | actionHandlers.hello = function (iframe, data, respond) { 20 | // Make iframe responsive 21 | iframe.style.width = '100%'; 22 | 23 | // Bugfix for Chrome: Force update of iframe width. If this is not done the 24 | // document size may not be updated before the content resizes. 25 | iframe.getBoundingClientRect(); 26 | 27 | // Tell iframe that it needs to resize when our window resizes 28 | var resize = function () { 29 | if (iframe.contentWindow) { 30 | // Limit resize calls to avoid flickering 31 | respond('resize'); 32 | } 33 | else { 34 | // Frame is gone, unregister. 35 | window.removeEventListener('resize', resize); 36 | } 37 | }; 38 | window.addEventListener('resize', resize, false); 39 | 40 | // Respond to let the iframe know we can resize it 41 | respond('hello'); 42 | }; 43 | 44 | /** 45 | * Prepare iframe resize. 46 | * 47 | * @private 48 | * @param {Object} iframe Element 49 | * @param {Object} data Payload 50 | * @param {Function} respond Send a response to the iframe 51 | */ 52 | actionHandlers.prepareResize = function (iframe, data, respond) { 53 | // Do not resize unless page and scrolling differs 54 | if (iframe.clientHeight !== data.scrollHeight || 55 | data.scrollHeight !== data.clientHeight) { 56 | 57 | // Reset iframe height, in case content has shrinked. 58 | iframe.style.height = data.clientHeight + 'px'; 59 | respond('resizePrepared'); 60 | } 61 | }; 62 | 63 | /** 64 | * Resize parent and iframe to desired height. 65 | * 66 | * @private 67 | * @param {Object} iframe Element 68 | * @param {Object} data Payload 69 | * @param {Function} respond Send a response to the iframe 70 | */ 71 | actionHandlers.resize = function (iframe, data) { 72 | // Resize iframe so all content is visible. Use scrollHeight to make sure we get everything 73 | iframe.style.height = data.scrollHeight + 'px'; 74 | }; 75 | 76 | /** 77 | * Keyup event handler. Exits full screen on escape. 78 | * 79 | * @param {Event} event 80 | */ 81 | var escape = function (event) { 82 | if (event.keyCode === 27) { 83 | exitFullScreen(); 84 | } 85 | }; 86 | 87 | // Listen for messages from iframes 88 | window.addEventListener('message', function receiveMessage(event) { 89 | if (event.data.context !== 'h5p') { 90 | return; // Only handle h5p requests. 91 | } 92 | 93 | // Find out who sent the message 94 | var iframe, iframes = document.getElementsByTagName('iframe'); 95 | for (var i = 0; i < iframes.length; i++) { 96 | if (iframes[i].contentWindow === event.source) { 97 | iframe = iframes[i]; 98 | break; 99 | } 100 | } 101 | 102 | if (!iframe) { 103 | return; // Cannot find sender 104 | } 105 | 106 | // Find action handler handler 107 | if (actionHandlers[event.data.action]) { 108 | actionHandlers[event.data.action](iframe, event.data, function respond(action, data) { 109 | if (data === undefined) { 110 | data = {}; 111 | } 112 | data.action = action; 113 | data.context = 'h5p'; 114 | event.source.postMessage(data, event.origin); 115 | }); 116 | } 117 | }, false); 118 | 119 | // Let h5p iframes know we're ready! 120 | var iframes = document.getElementsByTagName('iframe'); 121 | var ready = { 122 | context: 'h5p', 123 | action: 'ready' 124 | }; 125 | for (var i = 0; i < iframes.length; i++) { 126 | if (iframes[i].src.indexOf('h5p') !== -1) { 127 | iframes[i].contentWindow.postMessage(ready, '*'); 128 | } 129 | } 130 | 131 | })(); 132 | -------------------------------------------------------------------------------- /js/h5p-tooltip.js: -------------------------------------------------------------------------------- 1 | /*global H5P*/ 2 | H5P.Tooltip = (function () { 3 | 'use strict'; 4 | 5 | // Position (allowed and default) 6 | const Position = { 7 | allowed: ['top', 'bottom', 'left', 'right'], 8 | default: 'top' 9 | }; 10 | 11 | /** 12 | * Create an accessible tooltip 13 | * 14 | * @param {HTMLElement} triggeringElement The element that should trigger the tooltip 15 | * @param {Object} options Options for tooltip 16 | * @param {String} options.text The text to be displayed in the tooltip 17 | * If not set, will attempt to set text = options.tooltipSource of triggeringElement 18 | * @param {String[]} options.classes Extra css classes for the tooltip 19 | * @param {Boolean} options.ariaHidden Whether the hover should be read by screen readers or not (default: true) 20 | * @param {String} options.position Where the tooltip should appear in relation to the 21 | * triggeringElement. Accepted positions are "top" (default), "left", "right" and "bottom" 22 | * @param {String} options.tooltipSource 23 | * 24 | * @returns {object} returns all the public functions 25 | * 26 | * @constructor 27 | */ 28 | 29 | function Tooltip(triggeringElement, options) { 30 | // Make sure tooltips have unique id 31 | H5P.Tooltip.uniqueId += 1; 32 | const tooltipId = 'h5p-tooltip-' + H5P.Tooltip.uniqueId; 33 | 34 | // Default options 35 | options = options || {}; 36 | options.classes = options.classes || []; 37 | options.ariaHidden = options.ariaHidden || true; 38 | options.tooltipSource = options.tooltipSource || 'aria-label'; 39 | options.position = (options.position && Position.allowed.includes(options.position)) 40 | ? options.position 41 | : Position.default; 42 | 43 | // Add our internal classes 44 | options.classes.push('h5p-tooltip'); 45 | if (options.position === 'left' || options.position === 'right' ) { 46 | options.classes.push('h5p-tooltip-narrow'); 47 | } 48 | 49 | // Initiate state 50 | let hover = false; 51 | let focus = false; 52 | 53 | // Function used by the escape listener 54 | const hideOnEscape = function (event) { 55 | if (event.key === 'Escape') { 56 | tooltip.classList.remove('h5p-tooltip-visible'); 57 | } 58 | }; 59 | 60 | // Create element 61 | const tooltip = document.createElement('div'); 62 | tooltip.id = tooltipId; 63 | tooltip.role = 'tooltip'; 64 | tooltip.innerHTML = options.text || triggeringElement.getAttribute(options.tooltipSource) || ''; 65 | tooltip.setAttribute('aria-hidden', options.ariaHidden); 66 | tooltip.classList.add(...options.classes); 67 | 68 | document.body.appendChild(tooltip); 69 | 70 | // Aria-describedby will override aria-hidden 71 | if (!options.ariaHidden) { 72 | triggeringElement.setAttribute('aria-describedby', tooltipId); 73 | } 74 | 75 | // Use a mutation observer to listen for options.tooltipSource being 76 | // changed for the triggering element. If so, update the tooltip. 77 | // Mutation observer will be used even if the original elements 78 | // doesn't have any options.tooltipSource. 79 | this.observer = new MutationObserver(function (mutations) { 80 | const updatedText = mutations[0].target.getAttribute(options.tooltipSource); 81 | 82 | if (tooltip.parentNode === null) { 83 | triggeringElement.appendChild(tooltip); 84 | } 85 | 86 | tooltip.innerHTML = options.text || updatedText; 87 | 88 | if (tooltip.innerHTML.trim().length === 0 && tooltip.classList.contains('h5p-tooltip-visible')) { 89 | tooltip.classList.remove('h5p-tooltip-visible'); 90 | } 91 | 92 | }) 93 | this.observer.observe(triggeringElement, { 94 | attributes: true, 95 | attributeFilter: [options.tooltipSource, 'class'], 96 | }); 97 | 98 | // A reference to the H5P container (if any). If null, it means 99 | // this tooltip is not whithin an H5P. 100 | let h5pContainer; 101 | 102 | // Timer responsible for displaying the tooltip x ms after it has been 103 | // triggered (either by mouseenter or focusin) 104 | let showTooltipTimer; 105 | 106 | // This timer makes sure the tooltip is not hidden when the mouse 107 | // moves from the trigger to the tooltip. 108 | let triggerMouseLeaveTimer; 109 | 110 | /** 111 | * Makes the tooltip visible and activates it's functionality 112 | * 113 | * @param {UIEvent} event The triggering event 114 | */ 115 | const showTooltip = function (event, wait = true) { 116 | if (wait === true) { 117 | // We don't want to show the tooltip right away. 118 | // Adding a 300 ms waiting period here. 119 | showTooltipTimer = setTimeout(() => { 120 | showTooltip(event, false); 121 | }, 300); 122 | return; 123 | } 124 | 125 | // Don't show tooltip if it is empty 126 | if (tooltip.innerHTML.trim().length === 0) { 127 | return; 128 | } 129 | 130 | if (event.type === 'mouseenter') { 131 | hover = true; 132 | } 133 | else { 134 | focus = true; 135 | } 136 | 137 | // Reset placement 138 | tooltip.style.left = ''; 139 | tooltip.style.top = ''; 140 | 141 | tooltip.classList.add('h5p-tooltip-visible'); 142 | 143 | // Add listener to iframe body, as esc keypress would not be detected otherwise 144 | document.body.addEventListener('keydown', hideOnEscape, true); 145 | 146 | // The section below makes sure the tooltip is completely visible 147 | 148 | // H5P.Tooltip can be used both from within an H5P and elsewhere. 149 | // The below code is for figuring out the containing element. 150 | // h5pContainer has to be looked up the first time we show the tooltip, 151 | // since it might not be added to the DOM when H5P.Tooltip is invoked. 152 | if (h5pContainer === undefined) { 153 | // After the below, h5pContainer is either null or a reference to the 154 | // DOM element 155 | h5pContainer = triggeringElement.closest('.h5p-container'); 156 | } 157 | const rootRect = h5pContainer ? h5pContainer.getBoundingClientRect() : document.documentElement.getBoundingClientRect(); 158 | const triggerRect = triggeringElement.getBoundingClientRect(); 159 | let tooltipRect = tooltip.getBoundingClientRect(); 160 | 161 | if (options.position === 'top') { 162 | // Places it centered above 163 | tooltip.style.left = (triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2)) + 'px'; 164 | tooltip.style.top = (triggerRect.top - tooltipRect.height) + 'px'; 165 | } 166 | else if (options.position === 'bottom') { 167 | // Places it centered below 168 | tooltip.style.left = (triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2)) + 'px'; 169 | tooltip.style.top = triggerRect.bottom + 'px'; 170 | } 171 | else if (options.position === 'left') { 172 | tooltip.style.left = (triggerRect.left - tooltipRect.width) + 'px'; 173 | tooltip.style.top = (triggerRect.top + (triggerRect.height - tooltipRect.height) / 2) + 'px'; 174 | // We trust this option makes the tooltip being shown 175 | return; 176 | } 177 | else if (options.position === 'right') { 178 | tooltip.style.left = triggerRect.right + 'px'; 179 | tooltip.style.top = (triggerRect.top + (triggerRect.height - tooltipRect.height) / 2) + 'px'; 180 | // We trust this option makes the tooltip being shown 181 | return; 182 | } 183 | 184 | tooltipRect = tooltip.getBoundingClientRect(); 185 | const isVisible = tooltipRect.left >= 0 && 186 | tooltipRect.top >= 0 && 187 | tooltipRect.right <= rootRect.width && 188 | tooltipRect.bottom <= rootRect.height; 189 | 190 | if (!isVisible) { 191 | // The tooltip placement needs to be adjusted. This logic will move the 192 | // tooltip either left or right if it's placed outside the root element 193 | tooltipRect = tooltip.getBoundingClientRect(); 194 | if (tooltipRect.left < 0) { 195 | tooltip.style.left = 0; 196 | } 197 | else if (tooltipRect.right > rootRect.width) { 198 | tooltip.style.left = ''; 199 | tooltip.style.right = 0; 200 | } 201 | } 202 | }; 203 | 204 | 205 | /** 206 | * Hides the tooltip and removes listeners 207 | * 208 | * @param {UIEvent} event The triggering event 209 | */ 210 | const hideTooltip = function (event) { 211 | let hide = false; 212 | 213 | if (event.type === 'click') { 214 | hide = true; 215 | } 216 | else { 217 | if (event.type === 'mouseleave') { 218 | hover = false; 219 | } 220 | else { 221 | focus = false; 222 | } 223 | 224 | hide = (!hover && !focus); 225 | } 226 | 227 | // Only hide tooltip if neither hovered nor focused 228 | if (hide) { 229 | 230 | // We're hiding the tooltip. If the showTooltipTimer is running 231 | // we have to stop it. 232 | clearTimeout(showTooltipTimer); 233 | 234 | tooltip.classList.remove('h5p-tooltip-visible'); 235 | 236 | // Remove iframe body listener 237 | document.body.removeEventListener('keydown', hideOnEscape, true); 238 | } 239 | }; 240 | 241 | // Add event listeners to triggeringElement 242 | triggeringElement.addEventListener('mouseenter', showTooltip); 243 | triggeringElement.addEventListener('mouseleave', event => { 244 | triggerMouseLeaveTimer = setTimeout(() => { 245 | hideTooltip(event); 246 | }, 1); 247 | }); 248 | triggeringElement.addEventListener('focusin', showTooltip); 249 | triggeringElement.addEventListener('focusout', hideTooltip); 250 | triggeringElement.addEventListener('click', hideTooltip); 251 | tooltip.addEventListener('mouseenter', () => { 252 | clearTimeout(triggerMouseLeaveTimer); 253 | }); 254 | tooltip.addEventListener('mouseleave', hideTooltip); 255 | 256 | 257 | tooltip.addEventListener('click', function (event) { 258 | // Prevent clicks on the tooltip from triggering click 259 | // listeners on the triggering element 260 | event.stopPropagation(); 261 | event.preventDefault(); 262 | 263 | // Hide the tooltip when it is clicked 264 | hideTooltip(event); 265 | }); 266 | 267 | /** 268 | * Change the text displayed by the tooltip 269 | * 270 | * @param {String} text The new text to be displayed 271 | * Set to null to use options.tooltipSource of triggeringElement instead 272 | */ 273 | this.setText = function (text) { 274 | options.text = text; 275 | tooltip.innerHTML = options.text || triggeringElement.getAttribute(options.tooltipSource) || ''; 276 | }; 277 | 278 | /** 279 | * Hide the tooltip 280 | */ 281 | this.hide = function () { 282 | hover = focus = false; 283 | tooltip.classList.remove('h5p-tooltip-visible'); 284 | }; 285 | 286 | /** 287 | * Retrieve tooltip 288 | * 289 | * @return {HTMLElement} 290 | */ 291 | this.getElement = function () { 292 | return tooltip; 293 | }; 294 | 295 | /** 296 | * Remove tooltip 297 | */ 298 | this.remove = function () { 299 | this.observer?.disconnect(); 300 | tooltip.remove(); 301 | }; 302 | 303 | return { 304 | setText: this.setText, 305 | hide: this.hide, 306 | getElement: this.getElement, 307 | remove: this.remove, 308 | observer: this.observer, 309 | }; 310 | } 311 | 312 | return Tooltip; 313 | 314 | })(); 315 | 316 | H5P.Tooltip.uniqueId = -1; -------------------------------------------------------------------------------- /js/h5p-version.js: -------------------------------------------------------------------------------- 1 | H5P.Version = (function () { 2 | /** 3 | * Make it easy to keep track of version details. 4 | * 5 | * @class 6 | * @namespace H5P 7 | * @param {String} version 8 | */ 9 | function Version(version) { 10 | 11 | if (typeof version === 'string') { 12 | // Name version string (used by content upgrade) 13 | var versionSplit = version.split('.', 3); 14 | this.major =+ versionSplit[0]; 15 | this.minor =+ versionSplit[1]; 16 | } 17 | else { 18 | // Library objects (used by editor) 19 | if (version.localMajorVersion !== undefined) { 20 | this.major =+ version.localMajorVersion; 21 | this.minor =+ version.localMinorVersion; 22 | } 23 | else { 24 | this.major =+ version.majorVersion; 25 | this.minor =+ version.minorVersion; 26 | } 27 | } 28 | 29 | /** 30 | * Public. Custom string for this object. 31 | * 32 | * @returns {String} 33 | */ 34 | this.toString = function () { 35 | return version; 36 | }; 37 | } 38 | 39 | return Version; 40 | })(); 41 | -------------------------------------------------------------------------------- /js/h5p-x-api-event.js: -------------------------------------------------------------------------------- 1 | var H5P = window.H5P = window.H5P || {}; 2 | 3 | /** 4 | * Used for xAPI events. 5 | * 6 | * @class 7 | * @extends H5P.Event 8 | */ 9 | H5P.XAPIEvent = function () { 10 | H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); 11 | }; 12 | 13 | H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); 14 | H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; 15 | 16 | /** 17 | * Set scored result statements. 18 | * 19 | * @param {number} score 20 | * @param {number} maxScore 21 | * @param {object} instance 22 | * @param {boolean} completion 23 | * @param {boolean} success 24 | */ 25 | H5P.XAPIEvent.prototype.setScoredResult = function (score, maxScore, instance, completion, success) { 26 | this.data.statement.result = {}; 27 | 28 | if (typeof score !== 'undefined') { 29 | if (typeof maxScore === 'undefined') { 30 | this.data.statement.result.score = {'raw': score}; 31 | } 32 | else { 33 | this.data.statement.result.score = { 34 | 'min': 0, 35 | 'max': maxScore, 36 | 'raw': score 37 | }; 38 | if (maxScore > 0) { 39 | this.data.statement.result.score.scaled = Math.round(score / maxScore * 10000) / 10000; 40 | } 41 | } 42 | } 43 | 44 | if (typeof completion === 'undefined') { 45 | this.data.statement.result.completion = (this.getVerb() === 'completed' || this.getVerb() === 'answered'); 46 | } 47 | else { 48 | this.data.statement.result.completion = completion; 49 | } 50 | 51 | if (typeof success !== 'undefined') { 52 | this.data.statement.result.success = success; 53 | } 54 | 55 | if (instance && instance.activityStartTime) { 56 | var duration = Math.round((Date.now() - instance.activityStartTime ) / 10) / 100; 57 | // xAPI spec allows a precision of 0.01 seconds 58 | 59 | this.data.statement.result.duration = 'PT' + duration + 'S'; 60 | } 61 | }; 62 | 63 | /** 64 | * Set a verb. 65 | * 66 | * @param {string} verb 67 | * Verb in short form, one of the verbs defined at 68 | * {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary} 69 | * 70 | */ 71 | H5P.XAPIEvent.prototype.setVerb = function (verb) { 72 | if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { 73 | this.data.statement.verb = { 74 | 'id': 'http://adlnet.gov/expapi/verbs/' + verb, 75 | 'display': { 76 | 'en-US': verb 77 | } 78 | }; 79 | } 80 | else if (verb.id !== undefined) { 81 | this.data.statement.verb = verb; 82 | } 83 | }; 84 | 85 | /** 86 | * Get the statements verb id. 87 | * 88 | * @param {boolean} full 89 | * if true the full verb id prefixed by http://adlnet.gov/expapi/verbs/ 90 | * will be returned 91 | * @returns {string} 92 | * Verb or null if no verb with an id has been defined 93 | */ 94 | H5P.XAPIEvent.prototype.getVerb = function (full) { 95 | var statement = this.data.statement; 96 | if ('verb' in statement) { 97 | if (full === true) { 98 | return statement.verb; 99 | } 100 | return statement.verb.id.slice(31); 101 | } 102 | else { 103 | return null; 104 | } 105 | }; 106 | 107 | /** 108 | * Set the object part of the statement. 109 | * 110 | * The id is found automatically (the url to the content) 111 | * 112 | * @param {Object} instance 113 | * The H5P instance 114 | */ 115 | H5P.XAPIEvent.prototype.setObject = function (instance) { 116 | if (instance.contentId) { 117 | this.data.statement.object = { 118 | 'id': this.getContentXAPIId(instance), 119 | 'objectType': 'Activity', 120 | 'definition': { 121 | 'extensions': { 122 | 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId 123 | } 124 | } 125 | }; 126 | if (instance.subContentId) { 127 | this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId; 128 | // Don't set titles on main content, title should come from publishing platform 129 | if (typeof instance.getTitle === 'function') { 130 | this.data.statement.object.definition.name = { 131 | "en-US": instance.getTitle() 132 | }; 133 | } 134 | } 135 | else { 136 | var content = H5P.getContentForInstance(instance.contentId); 137 | if (content && content.metadata && content.metadata.title) { 138 | this.data.statement.object.definition.name = { 139 | "en-US": H5P.createTitle(content.metadata.title) 140 | }; 141 | } 142 | } 143 | } 144 | else { 145 | // Content types view always expect to have a contentId when they are displayed. 146 | // This is not the case if they are displayed in the editor as part of a preview. 147 | // The fix is to set an empty object with definition for the xAPI event, so all 148 | // the content types that rely on this does not have to handle it. This means 149 | // that content types that are being previewed will send xAPI completed events, 150 | // but since there are no scripts that catch these events in the editor, 151 | // this is not a problem. 152 | this.data.statement.object = { 153 | definition: {} 154 | }; 155 | } 156 | }; 157 | 158 | /** 159 | * Set the context part of the statement. 160 | * 161 | * @param {Object} instance 162 | * The H5P instance 163 | */ 164 | H5P.XAPIEvent.prototype.setContext = function (instance) { 165 | if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) { 166 | this.data.statement.context = { 167 | "contextActivities": { 168 | "parent": [ 169 | { 170 | "id": this.getContentXAPIId(instance.parent), 171 | "objectType": "Activity" 172 | } 173 | ] 174 | } 175 | }; 176 | } 177 | if (instance.libraryInfo) { 178 | if (this.data.statement.context === undefined) { 179 | this.data.statement.context = {"contextActivities":{}}; 180 | } 181 | this.data.statement.context.contextActivities.category = [ 182 | { 183 | "id": "http://h5p.org/libraries/" + instance.libraryInfo.versionedNameNoSpaces, 184 | "objectType": "Activity" 185 | } 186 | ]; 187 | } 188 | }; 189 | 190 | /** 191 | * Set the actor. Email and name will be added automatically. 192 | */ 193 | H5P.XAPIEvent.prototype.setActor = function () { 194 | if (H5PIntegration.user !== undefined) { 195 | this.data.statement.actor = { 196 | 'name': H5PIntegration.user.name, 197 | 'mbox': 'mailto:' + H5PIntegration.user.mail, 198 | 'objectType': 'Agent' 199 | }; 200 | } 201 | else { 202 | var uuid; 203 | try { 204 | if (localStorage.H5PUserUUID) { 205 | uuid = localStorage.H5PUserUUID; 206 | } 207 | else { 208 | uuid = H5P.createUUID(); 209 | localStorage.H5PUserUUID = uuid; 210 | } 211 | } 212 | catch (err) { 213 | // LocalStorage and Cookies are probably disabled. Do not track the user. 214 | uuid = 'not-trackable-' + H5P.createUUID(); 215 | } 216 | this.data.statement.actor = { 217 | 'account': { 218 | 'name': uuid, 219 | 'homePage': H5PIntegration.siteUrl 220 | }, 221 | 'objectType': 'Agent' 222 | }; 223 | } 224 | }; 225 | 226 | /** 227 | * Get the max value of the result - score part of the statement 228 | * 229 | * @returns {number} 230 | * The max score, or null if not defined 231 | */ 232 | H5P.XAPIEvent.prototype.getMaxScore = function () { 233 | return this.getVerifiedStatementValue(['result', 'score', 'max']); 234 | }; 235 | 236 | /** 237 | * Get the raw value of the result - score part of the statement 238 | * 239 | * @returns {number} 240 | * The score, or null if not defined 241 | */ 242 | H5P.XAPIEvent.prototype.getScore = function () { 243 | return this.getVerifiedStatementValue(['result', 'score', 'raw']); 244 | }; 245 | 246 | /** 247 | * Get content xAPI ID. 248 | * 249 | * @param {Object} instance 250 | * The H5P instance 251 | */ 252 | H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { 253 | var xAPIId; 254 | if (instance.contentId && H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId]) { 255 | xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; 256 | if (instance.subContentId) { 257 | xAPIId += '?subContentId=' + instance.subContentId; 258 | } 259 | } 260 | return xAPIId; 261 | }; 262 | 263 | /** 264 | * Check if this event is sent from a child (i.e not from grandchild) 265 | * 266 | * @return {Boolean} 267 | */ 268 | H5P.XAPIEvent.prototype.isFromChild = function () { 269 | var parentId = this.getVerifiedStatementValue(['context', 'contextActivities', 'parent', 0, 'id']); 270 | return !parentId || parentId.indexOf('subContentId') === -1; 271 | }; 272 | 273 | /** 274 | * Figure out if a property exists in the statement and return it 275 | * 276 | * @param {string[]} keys 277 | * List describing the property we're looking for. For instance 278 | * ['result', 'score', 'raw'] for result.score.raw 279 | * @returns {*} 280 | * The value of the property if it is set, null otherwise. 281 | */ 282 | H5P.XAPIEvent.prototype.getVerifiedStatementValue = function (keys) { 283 | var val = this.data.statement; 284 | for (var i = 0; i < keys.length; i++) { 285 | if (val[keys[i]] === undefined) { 286 | return null; 287 | } 288 | val = val[keys[i]]; 289 | } 290 | return val; 291 | }; 292 | 293 | /** 294 | * List of verbs defined at {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary} 295 | * 296 | * @type Array 297 | */ 298 | H5P.XAPIEvent.allowedXAPIVerbs = [ 299 | 'answered', 300 | 'asked', 301 | 'attempted', 302 | 'attended', 303 | 'commented', 304 | 'completed', 305 | 'exited', 306 | 'experienced', 307 | 'failed', 308 | 'imported', 309 | 'initialized', 310 | 'interacted', 311 | 'launched', 312 | 'mastered', 313 | 'passed', 314 | 'preferred', 315 | 'progressed', 316 | 'registered', 317 | 'responded', 318 | 'resumed', 319 | 'scored', 320 | 'shared', 321 | 'suspended', 322 | 'terminated', 323 | 'voided', 324 | 325 | // Custom verbs used for action toolbar below content 326 | 'downloaded', 327 | 'copied', 328 | 'accessed-reuse', 329 | 'accessed-embed', 330 | 'accessed-copyright' 331 | ]; 332 | -------------------------------------------------------------------------------- /js/h5p-x-api.js: -------------------------------------------------------------------------------- 1 | var H5P = window.H5P = window.H5P || {}; 2 | 3 | /** 4 | * The external event dispatcher. Others, outside of H5P may register and 5 | * listen for H5P Events here. 6 | * 7 | * @type {H5P.EventDispatcher} 8 | */ 9 | H5P.externalDispatcher = new H5P.EventDispatcher(); 10 | 11 | // EventDispatcher extensions 12 | 13 | /** 14 | * Helper function for triggering xAPI added to the EventDispatcher. 15 | * 16 | * @param {string} verb 17 | * The short id of the verb we want to trigger 18 | * @param {Oject} [extra] 19 | * Extra properties for the xAPI statement 20 | */ 21 | H5P.EventDispatcher.prototype.triggerXAPI = function (verb, extra) { 22 | this.trigger(this.createXAPIEventTemplate(verb, extra)); 23 | }; 24 | 25 | /** 26 | * Helper function to create event templates added to the EventDispatcher. 27 | * 28 | * Will in the future be used to add representations of the questions to the 29 | * statements. 30 | * 31 | * @param {string} verb 32 | * Verb id in short form 33 | * @param {Object} [extra] 34 | * Extra values to be added to the statement 35 | * @returns {H5P.XAPIEvent} 36 | * Instance 37 | */ 38 | H5P.EventDispatcher.prototype.createXAPIEventTemplate = function (verb, extra) { 39 | var event = new H5P.XAPIEvent(); 40 | 41 | event.setActor(); 42 | event.setVerb(verb); 43 | if (extra !== undefined) { 44 | for (var i in extra) { 45 | event.data.statement[i] = extra[i]; 46 | } 47 | } 48 | if (!('object' in event.data.statement)) { 49 | event.setObject(this); 50 | } 51 | if (!('context' in event.data.statement)) { 52 | event.setContext(this); 53 | } 54 | return event; 55 | }; 56 | 57 | /** 58 | * Helper function to create xAPI completed events 59 | * 60 | * DEPRECATED - USE triggerXAPIScored instead 61 | * 62 | * @deprecated 63 | * since 1.5, use triggerXAPIScored instead. 64 | * @param {number} score 65 | * Will be set as the 'raw' value of the score object 66 | * @param {number} maxScore 67 | * will be set as the "max" value of the score object 68 | * @param {boolean} success 69 | * will be set as the "success" value of the result object 70 | */ 71 | H5P.EventDispatcher.prototype.triggerXAPICompleted = function (score, maxScore, success) { 72 | this.triggerXAPIScored(score, maxScore, 'completed', true, success); 73 | }; 74 | 75 | /** 76 | * Helper function to create scored xAPI events 77 | * 78 | * @param {number} score 79 | * Will be set as the 'raw' value of the score object 80 | * @param {number} maxScore 81 | * Will be set as the "max" value of the score object 82 | * @param {string} verb 83 | * Short form of adl verb 84 | * @param {boolean} completion 85 | * Is this a statement from a completed activity? 86 | * @param {boolean} success 87 | * Is this a statement from an activity that was done successfully? 88 | */ 89 | H5P.EventDispatcher.prototype.triggerXAPIScored = function (score, maxScore, verb, completion, success) { 90 | var event = this.createXAPIEventTemplate(verb); 91 | event.setScoredResult(score, maxScore, this, completion, success); 92 | this.trigger(event); 93 | }; 94 | 95 | H5P.EventDispatcher.prototype.setActivityStarted = function () { 96 | if (this.activityStartTime === undefined) { 97 | // Don't trigger xAPI events in the editor 98 | if (this.contentId !== undefined && 99 | H5PIntegration.contents !== undefined && 100 | H5PIntegration.contents['cid-' + this.contentId] !== undefined) { 101 | this.triggerXAPI('attempted'); 102 | } 103 | this.activityStartTime = Date.now(); 104 | } 105 | }; 106 | 107 | /** 108 | * Internal H5P function listening for xAPI completed events and stores scores 109 | * 110 | * @param {H5P.XAPIEvent} event 111 | */ 112 | H5P.xAPICompletedListener = function (event) { 113 | if ((event.getVerb() === 'completed' || event.getVerb() === 'answered') && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) { 114 | var score = event.getScore(); 115 | var maxScore = event.getMaxScore(); 116 | var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); 117 | H5P.setFinished(contentId, score, maxScore); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /js/request-queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Queue requests and handle them at your convenience 3 | * 4 | * @type {RequestQueue} 5 | */ 6 | H5P.RequestQueue = (function ($, EventDispatcher) { 7 | /** 8 | * A queue for requests, will be automatically processed when regaining connection 9 | * 10 | * @param {boolean} [options.showToast] Show toast when losing or regaining connection 11 | * @constructor 12 | */ 13 | const RequestQueue = function (options) { 14 | EventDispatcher.call(this); 15 | this.processingQueue = false; 16 | options = options || {}; 17 | 18 | this.showToast = options.showToast; 19 | this.itemName = 'requestQueue'; 20 | }; 21 | 22 | /** 23 | * Add request to queue. Only supports posts currently. 24 | * 25 | * @param {string} url 26 | * @param {Object} data 27 | * @returns {boolean} 28 | */ 29 | RequestQueue.prototype.add = function (url, data) { 30 | if (!window.localStorage) { 31 | return false; 32 | } 33 | 34 | let storedStatements = this.getStoredRequests(); 35 | if (!storedStatements) { 36 | storedStatements = []; 37 | } 38 | 39 | storedStatements.push({ 40 | url: url, 41 | data: data, 42 | }); 43 | 44 | window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements)); 45 | 46 | this.trigger('requestQueued', { 47 | storedStatements: storedStatements, 48 | processingQueue: this.processingQueue, 49 | }); 50 | return true; 51 | }; 52 | 53 | /** 54 | * Get stored requests 55 | * 56 | * @returns {boolean|Array} Stored requests 57 | */ 58 | RequestQueue.prototype.getStoredRequests = function () { 59 | if (!window.localStorage) { 60 | return false; 61 | } 62 | 63 | const item = window.localStorage.getItem(this.itemName); 64 | if (!item) { 65 | return []; 66 | } 67 | 68 | return JSON.parse(item); 69 | }; 70 | 71 | /** 72 | * Clear stored requests 73 | * 74 | * @returns {boolean} True if the storage was successfully cleared 75 | */ 76 | RequestQueue.prototype.clearQueue = function () { 77 | if (!window.localStorage) { 78 | return false; 79 | } 80 | 81 | window.localStorage.removeItem(this.itemName); 82 | return true; 83 | }; 84 | 85 | /** 86 | * Start processing of requests queue 87 | * 88 | * @return {boolean} Returns false if it was not possible to resume processing queue 89 | */ 90 | RequestQueue.prototype.resumeQueue = function () { 91 | // Not supported 92 | if (!H5PIntegration || !window.navigator || !window.localStorage) { 93 | return false; 94 | } 95 | 96 | // Already processing 97 | if (this.processingQueue) { 98 | return false; 99 | } 100 | 101 | // Attempt to send queued requests 102 | const queue = this.getStoredRequests(); 103 | const queueLength = queue.length; 104 | 105 | // Clear storage, failed requests will be re-added 106 | this.clearQueue(); 107 | 108 | // No items left in queue 109 | if (!queueLength) { 110 | this.trigger('emptiedQueue', queue); 111 | return true; 112 | } 113 | 114 | // Make sure requests are not changed while they're being handled 115 | this.processingQueue = true; 116 | 117 | // Process queue in original order 118 | this.processQueue(queue); 119 | return true 120 | }; 121 | 122 | /** 123 | * Process first item in the request queue 124 | * 125 | * @param {Array} queue Request queue 126 | */ 127 | RequestQueue.prototype.processQueue = function (queue) { 128 | if (!queue.length) { 129 | return; 130 | } 131 | 132 | this.trigger('processingQueue'); 133 | 134 | // Make sure the requests are processed in a FIFO order 135 | const request = queue.shift(); 136 | 137 | const self = this; 138 | $.post(request.url, request.data) 139 | .fail(self.onQueuedRequestFail.bind(self, request)) 140 | .always(self.onQueuedRequestProcessed.bind(self, queue)) 141 | }; 142 | 143 | /** 144 | * Request fail handler 145 | * 146 | * @param {Object} request 147 | */ 148 | RequestQueue.prototype.onQueuedRequestFail = function (request) { 149 | // Queue the failed request again if we're offline 150 | if (!window.navigator.onLine) { 151 | this.add(request.url, request.data); 152 | } 153 | }; 154 | 155 | /** 156 | * An item in the queue was processed 157 | * 158 | * @param {Array} queue Queue that was processed 159 | */ 160 | RequestQueue.prototype.onQueuedRequestProcessed = function (queue) { 161 | if (queue.length) { 162 | this.processQueue(queue); 163 | return; 164 | } 165 | 166 | // Finished processing this queue 167 | this.processingQueue = false; 168 | 169 | // Run empty queue callback with next request queue 170 | const requestQueue = this.getStoredRequests(); 171 | this.trigger('queueEmptied', requestQueue); 172 | }; 173 | 174 | /** 175 | * Display toast message on the first content of current page 176 | * 177 | * @param {string} msg Message to display 178 | * @param {boolean} [forceShow] Force override showing the toast 179 | * @param {Object} [configOverride] Override toast message config 180 | */ 181 | RequestQueue.prototype.displayToastMessage = function (msg, forceShow, configOverride) { 182 | if (!this.showToast && !forceShow) { 183 | return; 184 | } 185 | 186 | const config = H5P.jQuery.extend(true, {}, { 187 | position: { 188 | horizontal : 'centered', 189 | vertical: 'centered', 190 | noOverflowX: true, 191 | } 192 | }, configOverride); 193 | 194 | H5P.attachToastTo(H5P.jQuery('.h5p-content:first')[0], msg, config); 195 | }; 196 | 197 | return RequestQueue; 198 | })(H5P.jQuery, H5P.EventDispatcher); 199 | 200 | /** 201 | * Request queue for retrying failing requests, will automatically retry them when you come online 202 | * 203 | * @type {offlineRequestQueue} 204 | */ 205 | H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) { 206 | 207 | /** 208 | * Constructor 209 | * 210 | * @param {Object} [options] Options for offline request queue 211 | * @param {Object} [options.instance] The H5P instance which UI components are placed within 212 | */ 213 | const offlineRequestQueue = function (options) { 214 | const requestQueue = new RequestQueue(); 215 | 216 | // We could handle requests from previous pages here, but instead we throw them away 217 | requestQueue.clearQueue(); 218 | 219 | let startTime = null; 220 | const retryIntervals = [10, 20, 40, 60, 120, 300, 600]; 221 | let intervalIndex = -1; 222 | let currentInterval = null; 223 | let isAttached = false; 224 | let isShowing = false; 225 | let isLoading = false; 226 | const instance = options.instance; 227 | 228 | const offlineDialog = new Dialog({ 229 | headerText: H5P.t('offlineDialogHeader'), 230 | dialogText: H5P.t('offlineDialogBody'), 231 | confirmText: H5P.t('offlineDialogRetryButtonLabel'), 232 | hideCancel: true, 233 | hideExit: true, 234 | classes: ['offline'], 235 | instance: instance, 236 | skipRestoreFocus: true, 237 | }); 238 | 239 | const dialog = offlineDialog.getElement(); 240 | 241 | // Add retry text to body 242 | const countDownText = document.createElement('div'); 243 | countDownText.classList.add('count-down'); 244 | countDownText.innerHTML = H5P.t('offlineDialogRetryMessage') 245 | .replace(':num', '0'); 246 | 247 | dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText); 248 | const countDownNum = countDownText.querySelector('.count-down-num'); 249 | 250 | // Create throbber 251 | const throbberWrapper = document.createElement('div'); 252 | throbberWrapper.classList.add('throbber-wrapper'); 253 | const throbber = document.createElement('div'); 254 | throbber.classList.add('sending-requests-throbber'); 255 | throbberWrapper.appendChild(throbber); 256 | 257 | requestQueue.on('requestQueued', function (e) { 258 | // Already processing queue, wait until queue has finished processing before showing dialog 259 | if (e.data && e.data.processingQueue) { 260 | return; 261 | } 262 | 263 | if (!isAttached) { 264 | const rootContent = document.body.querySelector('.h5p-content'); 265 | if (!rootContent) { 266 | return; 267 | } 268 | offlineDialog.appendTo(rootContent); 269 | rootContent.appendChild(throbberWrapper); 270 | isAttached = true; 271 | } 272 | 273 | startCountDown(); 274 | }.bind(this)); 275 | 276 | requestQueue.on('queueEmptied', function (e) { 277 | if (e.data && e.data.length) { 278 | // New requests were added while processing queue or requests failed again. Re-queue requests. 279 | startCountDown(true); 280 | return; 281 | } 282 | 283 | // Successfully emptied queue 284 | clearInterval(currentInterval); 285 | toggleThrobber(false); 286 | intervalIndex = -1; 287 | if (isShowing) { 288 | offlineDialog.hide(); 289 | isShowing = false; 290 | } 291 | 292 | requestQueue.displayToastMessage( 293 | H5P.t('offlineSuccessfulSubmit'), 294 | true, 295 | { 296 | position: { 297 | vertical: 'top', 298 | offsetVertical: '100', 299 | } 300 | } 301 | ); 302 | 303 | }.bind(this)); 304 | 305 | offlineDialog.on('confirmed', function () { 306 | // Show dialog on next render in case it is being hidden by the 'confirm' button 307 | isShowing = false; 308 | setTimeout(function () { 309 | retryRequests(); 310 | }, 100); 311 | }.bind(this)); 312 | 313 | // Initialize listener for when requests are added to queue 314 | window.addEventListener('online', function () { 315 | retryRequests(); 316 | }.bind(this)); 317 | 318 | // Listen for queued requests outside the iframe 319 | window.addEventListener('message', function (event) { 320 | const isValidQueueEvent = window.parent === event.source 321 | && event.data.context === 'h5p' 322 | && event.data.action === 'queueRequest'; 323 | 324 | if (!isValidQueueEvent) { 325 | return; 326 | } 327 | 328 | this.add(event.data.url, event.data.data); 329 | }.bind(this)); 330 | 331 | /** 332 | * Toggle throbber visibility 333 | * 334 | * @param {boolean} [forceShow] Will force throbber visibility if set 335 | */ 336 | const toggleThrobber = function (forceShow) { 337 | isLoading = !isLoading; 338 | if (forceShow !== undefined) { 339 | isLoading = forceShow; 340 | } 341 | 342 | if (isLoading && isShowing) { 343 | offlineDialog.hide(); 344 | isShowing = false; 345 | } 346 | 347 | if (isLoading) { 348 | throbberWrapper.classList.add('show'); 349 | } 350 | else { 351 | throbberWrapper.classList.remove('show'); 352 | } 353 | }; 354 | /** 355 | * Retries the failed requests 356 | */ 357 | const retryRequests = function () { 358 | clearInterval(currentInterval); 359 | toggleThrobber(true); 360 | requestQueue.resumeQueue(); 361 | }; 362 | 363 | /** 364 | * Increments retry interval 365 | */ 366 | const incrementRetryInterval = function () { 367 | intervalIndex += 1; 368 | if (intervalIndex >= retryIntervals.length) { 369 | intervalIndex = retryIntervals.length - 1; 370 | } 371 | }; 372 | 373 | /** 374 | * Starts counting down to retrying queued requests. 375 | * 376 | * @param forceDelayedShow 377 | */ 378 | const startCountDown = function (forceDelayedShow) { 379 | // Already showing, wait for retry 380 | if (isShowing) { 381 | return; 382 | } 383 | 384 | toggleThrobber(false); 385 | if (!isShowing) { 386 | if (forceDelayedShow) { 387 | // Must force delayed show since dialog may be hiding, and confirmation dialog does not 388 | // support this. 389 | setTimeout(function () { 390 | offlineDialog.show(0); 391 | }, 100); 392 | } 393 | else { 394 | offlineDialog.show(0); 395 | } 396 | } 397 | isShowing = true; 398 | startTime = new Date().getTime(); 399 | incrementRetryInterval(); 400 | clearInterval(currentInterval); 401 | currentInterval = setInterval(updateCountDown, 100); 402 | }; 403 | 404 | /** 405 | * Updates the count down timer. Retries requests when time expires. 406 | */ 407 | const updateCountDown = function () { 408 | const time = new Date().getTime(); 409 | const timeElapsed = Math.floor((time - startTime) / 1000); 410 | const timeLeft = retryIntervals[intervalIndex] - timeElapsed; 411 | countDownNum.textContent = timeLeft.toString(); 412 | 413 | // Retry interval reached, retry requests 414 | if (timeLeft <= 0) { 415 | retryRequests(); 416 | } 417 | }; 418 | 419 | /** 420 | * Add request to offline request queue. Only supports posts for now. 421 | * 422 | * @param {string} url The request url 423 | * @param {Object} data The request data 424 | */ 425 | this.add = function (url, data) { 426 | // Only queue request if it failed because we are offline 427 | if (window.navigator.onLine) { 428 | return false; 429 | } 430 | 431 | requestQueue.add(url, data); 432 | }; 433 | }; 434 | 435 | return offlineRequestQueue; 436 | })(H5P.RequestQueue, H5P.ConfirmationDialog); 437 | -------------------------------------------------------------------------------- /js/settings/h5p-disable-hub.js: -------------------------------------------------------------------------------- 1 | /* global H5PDisableHubData */ 2 | 3 | /** 4 | * Global data for disable hub functionality 5 | * 6 | * @typedef {object} H5PDisableHubData Data passed in from the backend 7 | * 8 | * @property {string} selector Selector for the disable hub check-button 9 | * @property {string} overlaySelector Selector for the element that the confirmation dialog will mask 10 | * @property {Array} errors Errors found with the current server setup 11 | * 12 | * @property {string} header Header of the confirmation dialog 13 | * @property {string} confirmationDialogMsg Body of the confirmation dialog 14 | * @property {string} cancelLabel Cancel label of the confirmation dialog 15 | * @property {string} confirmLabel Confirm button label of the confirmation dialog 16 | * 17 | */ 18 | /** 19 | * Utility that makes it possible to force the user to confirm that he really 20 | * wants to use the H5P hub without proper server settings. 21 | */ 22 | (function ($) { 23 | 24 | $(document).on('ready', function () { 25 | 26 | // No data found 27 | if (!H5PDisableHubData) { 28 | return; 29 | } 30 | 31 | // No errors found, no need for confirmation dialog 32 | if (!H5PDisableHubData.errors || !H5PDisableHubData.errors.length) { 33 | return; 34 | } 35 | 36 | H5PDisableHubData.selector = H5PDisableHubData.selector || 37 | '.h5p-settings-disable-hub-checkbox'; 38 | H5PDisableHubData.overlaySelector = H5PDisableHubData.overlaySelector || 39 | '.h5p-settings-container'; 40 | 41 | var dialogHtml = '
    ' + 42 | '

    ' + H5PDisableHubData.errors.join('

    ') + '

    ' + 43 | '

    ' + H5PDisableHubData.confirmationDialogMsg + '

    '; 44 | 45 | // Create confirmation dialog, make sure to include translations 46 | var confirmationDialog = new H5P.ConfirmationDialog({ 47 | headerText: H5PDisableHubData.header, 48 | dialogText: dialogHtml, 49 | cancelText: H5PDisableHubData.cancelLabel, 50 | confirmText: H5PDisableHubData.confirmLabel 51 | }).appendTo($(H5PDisableHubData.overlaySelector).get(0)); 52 | 53 | confirmationDialog.on('confirmed', function () { 54 | enableButton.get(0).checked = true; 55 | }); 56 | 57 | confirmationDialog.on('canceled', function () { 58 | enableButton.get(0).checked = false; 59 | }); 60 | 61 | var enableButton = $(H5PDisableHubData.selector); 62 | enableButton.change(function () { 63 | if ($(this).is(':checked')) { 64 | confirmationDialog.show(enableButton.offset().top); 65 | } 66 | }); 67 | }); 68 | })(H5P.jQuery); 69 | -------------------------------------------------------------------------------- /styles/h5p-admin.css: -------------------------------------------------------------------------------- 1 | /* Administration interface styling */ 2 | 3 | .h5p-content { 4 | border: 1px solid #DDD; 5 | border-radius: 3px; 6 | padding: 10px; 7 | } 8 | 9 | .h5p-admin-table, 10 | .h5p-admin-table > tbody { 11 | border: none; 12 | width: 100%; 13 | } 14 | 15 | .h5p-admin-table tr:nth-child(odd), 16 | .h5p-data-view tr:nth-child(odd) { 17 | background-color: #F9F9F9; 18 | } 19 | .h5p-admin-table tbody tr:hover { 20 | background-color: #EEE; 21 | } 22 | .h5p-admin-table.empty { 23 | padding: 1em; 24 | background-color: #EEE; 25 | font-size: 1.2em; 26 | font-weight: bold; 27 | } 28 | 29 | .h5p-admin-table.libraries th:last-child, 30 | .h5p-admin-table.libraries td:last-child { 31 | text-align: right; 32 | } 33 | 34 | .h5p-admin-buttons-wrapper { 35 | white-space: nowrap; 36 | } 37 | 38 | .h5p-admin-table.libraries button { 39 | font-size: 2em; 40 | cursor: pointer; 41 | border: 1px solid #AAA; 42 | border-radius: .2em; 43 | background-color: #e0e0e0; 44 | text-shadow: 0 0 0.5em #fff; 45 | padding: 0; 46 | line-height: 1em; 47 | width: 1.125em; 48 | height: 1.05em; 49 | text-indent: -0.125em; 50 | margin: 0.125em 0.125em 0 0.125em; 51 | } 52 | .h5p-admin-upgrade-library:before { 53 | font-family: 'H5P'; 54 | content: "\e888"; 55 | } 56 | .h5p-admin-view-library:before { 57 | font-family: 'H5P'; 58 | content: "\e889"; 59 | } 60 | .h5p-admin-delete-library:before { 61 | font-family: 'H5P'; 62 | content: "\e890"; 63 | } 64 | 65 | .h5p-admin-table.libraries button:hover { 66 | background-color: #d0d0d0; 67 | } 68 | .h5p-admin-table.libraries button:disabled:hover { 69 | background-color: #e0e0e0; 70 | cursor: default; 71 | } 72 | 73 | .h5p-admin-upgrade-library { 74 | color: #339900; 75 | } 76 | .h5p-admin-view-library { 77 | color: #0066cc; 78 | } 79 | .h5p-admin-delete-library { 80 | color: #990000; 81 | } 82 | .h5p-admin-delete-library:disabled, 83 | .h5p-admin-upgrade-library:disabled { 84 | cursor: default; 85 | color: #c0c0c0; 86 | } 87 | 88 | .h5p-library-info { 89 | padding: 1em 1em; 90 | margin: 1em 0; 91 | 92 | width: 350px; 93 | 94 | border: 1px solid #DDD; 95 | border-radius: 3px; 96 | } 97 | 98 | /* Labeled field (label + value) */ 99 | .h5p-labeled-field { 100 | border-bottom: 1px solid #ccc; 101 | } 102 | .h5p-labeled-field:last-child { 103 | border-bottom: none; 104 | } 105 | 106 | .h5p-labeled-field .h5p-label { 107 | display: inline-block; 108 | min-width: 150px; 109 | font-size: 1.2em; 110 | font-weight: bold; 111 | padding: 0.2em; 112 | } 113 | 114 | .h5p-labeled-field .h5p-value { 115 | display: inline-block; 116 | padding: 0.2em; 117 | } 118 | 119 | /* Search element */ 120 | .h5p-content-search { 121 | display: inline-block; 122 | position: relative; 123 | 124 | width: 100%; 125 | padding: 5px 0; 126 | margin-top: 10px; 127 | 128 | border: 1px solid #CCC; 129 | border-radius: 3px; 130 | box-shadow: 2px 2px 5px #888888; 131 | } 132 | .h5p-content-search:before { 133 | font-family: 'H5P'; 134 | vertical-align: bottom; 135 | content: "\e88a"; 136 | font-size: 2em; 137 | line-height: 1.25em; 138 | } 139 | .h5p-content-search input { 140 | font-size: 120%; 141 | line-height: 120%; 142 | } 143 | .h5p-admin-search-results { 144 | margin-left: 10px; 145 | color: #888; 146 | } 147 | 148 | .h5p-admin-pager-size-selector { 149 | position: absolute; 150 | right: 10px; 151 | top: .75em; 152 | display: inline-block; 153 | } 154 | .h5p-admin-pager-size-selector > span { 155 | padding: 5px; 156 | margin-left: 10px; 157 | cursor: pointer; 158 | border: 1px solid #CCC; 159 | border-radius: 3px; 160 | } 161 | .h5p-admin-pager-size-selector > span.selected { 162 | background-color: #edf5fa; 163 | } 164 | .h5p-admin-pager-size-selector > span:hover { 165 | background-color: #555; 166 | color: #FFF; 167 | } 168 | 169 | /* Generic "javascript"-action button */ 170 | button.h5p-admin { 171 | border: 1px solid #AAA; 172 | border-radius: 5px; 173 | padding: 3px 10px; 174 | background-color: #EEE; 175 | cursor: pointer; 176 | display: inline-block; 177 | text-align: center; 178 | color: #222; 179 | } 180 | button.h5p-admin:hover { 181 | background-color: #555; 182 | color: #FFF; 183 | } 184 | button.h5p-admin.disabled, 185 | button.h5p-admin.disabled:hover { 186 | cursor: auto; 187 | color: #CCC; 188 | background-color: #FFF; 189 | } 190 | 191 | /* Pager element */ 192 | .h5p-content-pager { 193 | display: inline-block; 194 | border: 1px solid #CCC; 195 | border-radius: 3px; 196 | box-shadow: 2px 2px 5px #888888; 197 | width: 100%; 198 | text-align: center; 199 | padding: 3px 0; 200 | } 201 | .h5p-content-pager > button { 202 | min-width: 80px; 203 | font-size: 130%; 204 | line-height: 130%; 205 | border: none; 206 | background: none; 207 | font-family: 'H5P'; 208 | font-size: 1.4em; 209 | } 210 | .h5p-content-pager > button:focus { 211 | outline: 0; 212 | } 213 | .h5p-content-pager > button:last-child { 214 | margin-left: 10px; 215 | } 216 | .h5p-content-pager > .pager-info { 217 | cursor: pointer; 218 | padding: 5px; 219 | border-radius: 3px; 220 | } 221 | .h5p-content-pager > .pager-info:hover { 222 | background-color: #555; 223 | color: #FFF; 224 | } 225 | .h5p-content-pager > .pager-info, 226 | .h5p-content-pager > .h5p-pager-goto { 227 | margin: 0 10px; 228 | line-height: 130%; 229 | display: inline-block; 230 | } 231 | 232 | .h5p-admin-header { 233 | margin-top: 1.5em; 234 | } 235 | #h5p-library-upload-form.h5p-admin-upload-libraries-form, 236 | #h5p-content-type-cache-update-form.h5p-admin-upload-libraries-form { 237 | position: relative; 238 | margin: 0; 239 | 240 | } 241 | .h5p-admin-upload-libraries-form .form-submit { 242 | position: absolute; 243 | top: 0; 244 | right: 0; 245 | } 246 | .h5p-spinner { 247 | padding: 0 0.5em; 248 | font-size: 1.5em; 249 | font-weight: bold; 250 | } 251 | #h5p-admin-container .h5p-admin-center { 252 | text-align: center; 253 | } 254 | .h5p-pagination { 255 | text-align: center; 256 | } 257 | .h5p-pagination > span, .h5p-pagination > input { 258 | margin: 0 1em; 259 | } 260 | .h5p-data-view input[type="text"] { 261 | margin-bottom: 0.5em; 262 | margin-right: 0.5em; 263 | float: left; 264 | } 265 | .h5p-data-view input[type="text"]::-ms-clear { 266 | display: none; 267 | } 268 | 269 | .h5p-data-view .h5p-others-contents-toggler-wrapper { 270 | float: right; 271 | line-height: 2; 272 | margin-right: 0.5em; 273 | } 274 | 275 | .h5p-data-view .h5p-others-contents-toggler-label { 276 | font-size: 14px; 277 | } 278 | 279 | .h5p-data-view .h5p-others-contents-toggler { 280 | margin-right: 0.5em; 281 | } 282 | 283 | .h5p-data-view th[role="button"] { 284 | cursor: pointer; 285 | } 286 | .h5p-data-view th[role="button"].h5p-sort:after, 287 | .h5p-data-view th[role="button"]:hover:after, 288 | .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after { 289 | content: "\25BE"; 290 | position: relative; 291 | left: 0.5em; 292 | top: -1px; 293 | } 294 | .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:after, 295 | .h5p-data-view th[role="button"].h5p-sort:hover:after { 296 | content: "\25B4"; 297 | top: -2px; 298 | } 299 | .h5p-data-view th[role="button"]:hover:after, 300 | .h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after, 301 | .h5p-data-view th[role="button"].h5p-sort:hover:after { 302 | color: #999; 303 | } 304 | .h5p-data-view .h5p-facet { 305 | cursor: pointer; 306 | color: #0073aa; 307 | outline: none; 308 | } 309 | .h5p-data-view .h5p-facet:hover, 310 | .h5p-data-view .h5p-facet:active { 311 | color: #00a0d2; 312 | } 313 | .h5p-data-view .h5p-facet:focus { 314 | color: #124964; 315 | box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8); 316 | } 317 | .h5p-data-view .h5p-facet-wrapper { 318 | line-height: 23px; 319 | } 320 | .h5p-data-view .h5p-facet-tag { 321 | margin: 2px 0 0 0.5em; 322 | font-size: 12px; 323 | background: #e8e8e8; 324 | border: 1px solid #cbcbcc; 325 | border-radius: 5px; 326 | color: #5d5d5d; 327 | padding: 0 24px 0 10px; 328 | display: inline-block; 329 | position: relative; 330 | } 331 | .h5p-data-view .h5p-facet-tag > span { 332 | position: absolute; 333 | right: 0; 334 | top: auto; 335 | bottom: auto; 336 | font-size: 18px; 337 | color: #a2a2a2; 338 | outline: none; 339 | width: 21px; 340 | text-indent: 4px; 341 | letter-spacing: 10px; 342 | overflow: hidden; 343 | cursor: pointer; 344 | } 345 | .h5p-data-view .h5p-facet-tag > span:before { 346 | content: "×"; 347 | font-weight: bold; 348 | } 349 | .h5p-data-view .h5p-facet-tag > span:hover, 350 | .h5p-data-view .h5p-facet-tag > span:focus { 351 | color: #a20000; 352 | } 353 | .h5p-data-view .h5p-facet-tag > span:active { 354 | color: #d20000; 355 | } 356 | .content-upgrade-log { 357 | color: red; 358 | } 359 | -------------------------------------------------------------------------------- /styles/h5p-confirmation-dialog.css: -------------------------------------------------------------------------------- 1 | .h5p-confirmation-dialog-background { 2 | position: fixed; 3 | height: 100%; 4 | width: 100%; 5 | left: 0; 6 | top: 0; 7 | 8 | background: rgba(44, 44, 44, 0.9); 9 | opacity: 1; 10 | visibility: visible; 11 | -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0s; 12 | transition: opacity 0.1s linear 0s, visibility 0s linear 0s; 13 | 14 | z-index: 201; 15 | } 16 | 17 | .h5p-confirmation-dialog-background.hidden { 18 | display: none; 19 | } 20 | 21 | .h5p-confirmation-dialog-background.hiding { 22 | opacity: 0; 23 | visibility: hidden; 24 | -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0.1s; 25 | transition: opacity 0.1s linear 0s, visibility 0s linear 0.1s; 26 | } 27 | 28 | .h5p-confirmation-dialog-popup:focus { 29 | outline: none; 30 | } 31 | 32 | .h5p-confirmation-dialog-popup { 33 | position: absolute; 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: center; 37 | 38 | box-sizing: border-box; 39 | max-width: 35em; 40 | min-width: 25em; 41 | 42 | top: 2em; 43 | left: 50%; 44 | -webkit-transform: translate(-50%, 0%); 45 | -ms-transform: translate(-50%, 0%); 46 | transform: translate(-50%, 0%); 47 | 48 | color: #555; 49 | box-shadow: 0 0 6px 6px rgba(10,10,10,0.3); 50 | 51 | -webkit-transition: transform 0.1s ease-in; 52 | transition: transform 0.1s ease-in; 53 | } 54 | 55 | .h5p-confirmation-dialog-popup.hidden { 56 | -webkit-transform: translate(-50%, 50%); 57 | -ms-transform: translate(-50%, 50%); 58 | transform: translate(-50%, 50%); 59 | } 60 | 61 | .h5p-confirmation-dialog-header { 62 | padding: 1.5em; 63 | background: #fff; 64 | color: #356593; 65 | } 66 | 67 | .h5p-confirmation-dialog-header-text { 68 | font-size: 1.25em; 69 | } 70 | 71 | .h5p-confirmation-dialog-body { 72 | background: #fafbfc; 73 | border-top: solid 1px #dde0e9; 74 | padding: 1.25em 1.5em; 75 | } 76 | 77 | .h5p-confirmation-dialog-text { 78 | margin-bottom: 1.5em; 79 | } 80 | 81 | .h5p-confirmation-dialog-buttons { 82 | float: right; 83 | } 84 | 85 | button.h5p-confirmation-dialog-exit:visited, 86 | button.h5p-confirmation-dialog-exit:link, 87 | button.h5p-confirmation-dialog-exit { 88 | position: absolute; 89 | background: none; 90 | border: none; 91 | font-size: 2.5em; 92 | top: -0.9em; 93 | right: -1.15em; 94 | color: #fff; 95 | cursor: pointer; 96 | text-decoration: none; 97 | } 98 | 99 | button.h5p-confirmation-dialog-exit:focus, 100 | button.h5p-confirmation-dialog-exit:hover { 101 | color: #E4ECF5; 102 | } 103 | 104 | .h5p-confirmation-dialog-exit:before { 105 | font-family: "H5P"; 106 | content: "\e890"; 107 | } 108 | 109 | .h5p-core-button.h5p-confirmation-dialog-confirm-button { 110 | @media screen and (max-width: 576px) { 111 | padding-left: 1.5rem; 112 | 113 | span { 114 | display: none; 115 | } 116 | } 117 | 118 | padding-left: 0.75em; 119 | margin-bottom: 0; 120 | } 121 | 122 | .h5p-core-button.h5p-confirmation-dialog-confirm-button:before { 123 | content: "\e601"; 124 | margin-top: -6px; 125 | display: inline-block; 126 | } 127 | 128 | .h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-buttons { 129 | float: none; 130 | text-align: center; 131 | } 132 | 133 | .h5p-confirmation-dialog-popup.offline .count-down { 134 | font-family: Arial; 135 | margin-top: 0.15em; 136 | color: #000; 137 | } 138 | 139 | .h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-confirm-button:before { 140 | content: "\e90b"; 141 | font-weight: normal; 142 | vertical-align: text-bottom; 143 | } 144 | 145 | .throbber-wrapper { 146 | display: none; 147 | position: absolute; 148 | height: 100%; 149 | width: 100%; 150 | top: 0; 151 | left: 0; 152 | z-index: 1; 153 | background: rgba(44, 44, 44, 0.9); 154 | } 155 | 156 | .throbber-wrapper.show { 157 | display: block; 158 | } 159 | 160 | .throbber-wrapper .throbber-container { 161 | position: absolute; 162 | top: 50%; 163 | left: 50%; 164 | transform: translate(-50%, -50%); 165 | } 166 | 167 | .throbber-wrapper .sending-requests-throbber{ 168 | position: absolute; 169 | top: 7em; 170 | left: 50%; 171 | transform: translateX(-50%); 172 | } 173 | 174 | .throbber-wrapper .sending-requests-throbber:before { 175 | display: block; 176 | font-family: 'H5P'; 177 | content: "\e90b"; 178 | color: white; 179 | font-size: 10em; 180 | animation: request-throbber 1.5s infinite linear; 181 | } 182 | 183 | @keyframes request-throbber { 184 | from { 185 | transform: rotate(0); 186 | } 187 | 188 | to { 189 | transform: rotate(359deg); 190 | } 191 | } 192 | 193 | @media (prefers-reduced-motion) { 194 | .h5p-confirmation-dialog-background { 195 | -webkit-transition: none; 196 | transition: none; 197 | 198 | .h5p-confirmation-dialog-popup { 199 | -webkit-transition: none; 200 | transition: none; 201 | } 202 | 203 | &.hiding { 204 | .h5p-confirmation-dialog-popup { 205 | opacity: 0; 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /styles/h5p-core-button.css: -------------------------------------------------------------------------------- 1 | button.h5p-core-button:visited, 2 | button.h5p-core-button:link, 3 | button.h5p-core-button { 4 | font-family: "Open Sans", sans-serif; 5 | font-weight: 600; 6 | font-size: 1em; 7 | line-height: 1.2; 8 | padding: 0.5em 1.25em; 9 | border-radius: 2em; 10 | 11 | background: #2579c6; 12 | color: #fff; 13 | 14 | cursor: pointer; 15 | border: none; 16 | box-shadow: none; 17 | outline: none; 18 | 19 | display: inline-block; 20 | text-align: center; 21 | text-shadow: none; 22 | vertical-align: baseline; 23 | text-decoration: none; 24 | 25 | -webkit-transition: initial; 26 | transition: initial; 27 | } 28 | button.h5p-core-button:focus { 29 | background: #1f67a8; 30 | } 31 | button.h5p-core-button:hover { 32 | background: rgba(31, 103, 168, 0.83); 33 | } 34 | button.h5p-core-button:active { 35 | background: #104888; 36 | } 37 | button.h5p-core-button:before { 38 | font-family: 'H5P'; 39 | padding-right: 0.15em; 40 | font-size: 1.5em; 41 | vertical-align: middle; 42 | line-height: 0.7; 43 | } 44 | button.h5p-core-cancel-button:visited, 45 | button.h5p-core-cancel-button:link, 46 | button.h5p-core-cancel-button { 47 | border: none; 48 | background: none; 49 | color: #a00; 50 | margin-right: 1em; 51 | font-size: 1em; 52 | text-decoration: none; 53 | cursor: pointer; 54 | } 55 | button.h5p-core-cancel-button:hover, 56 | button.h5p-core-cancel-button:focus { 57 | background: none; 58 | border: none; 59 | color: #e40000; 60 | } 61 | -------------------------------------------------------------------------------- /styles/h5p-table.css: -------------------------------------------------------------------------------- 1 | /* Table styling for content types to imitate ckeditor 5 */ 2 | .h5p-iframe { 3 | /* The figure around the table */ 4 | figure.table { 5 | display: table; 6 | table-layout: fixed; 7 | margin: 0 auto; 8 | padding: 0; 9 | float: left; 10 | 11 | /* The actual table */ 12 | table { 13 | border-collapse: collapse; 14 | height: 100%; 15 | width: 100%; 16 | border-spacing: 0; 17 | border-width: 1px; 18 | border-color: #494949; 19 | 20 | td, th { 21 | padding: 1px; 22 | border-color: #494949; 23 | border-bottom-style: solid; 24 | } 25 | 26 | td { 27 | border-width: 0.083em; 28 | } 29 | 30 | th { 31 | text-align: left; 32 | border-width: .167em; 33 | } 34 | 35 | tr:last-child > td { 36 | border-bottom-style: none; 37 | } 38 | } 39 | 40 | figcaption { 41 | background-color: transparent; 42 | caption-side: top; 43 | color: #333; 44 | display: table-caption; 45 | font-size: .75em; 46 | outline-offset: -1px; 47 | padding: .6em; 48 | text-align: center; 49 | word-break: break-word; 50 | } 51 | } 52 | 53 | .table-overflow-protection { 54 | clear: both; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /styles/h5p-tooltip.css: -------------------------------------------------------------------------------- 1 | .h5p-tooltip { 2 | display: none; 3 | position: absolute; 4 | 5 | z-index: 110; 6 | 7 | font-size: 0.8rem; 8 | line-height: 1.2; 9 | text-align: left; 10 | 11 | padding: 0.25rem 0.5rem; 12 | white-space: normal; 13 | width: max-content; 14 | max-width: min(300px, 90%); 15 | 16 | background: #000; 17 | color: #FFF; 18 | 19 | cursor: default; 20 | } 21 | .h5p-tooltip-narrow { 22 | max-width: min(300px, 70%); 23 | } 24 | .h5p-tooltip-visible { 25 | display: block; 26 | } 27 | --------------------------------------------------------------------------------