Skip to content

Commit

Permalink
Merge pull request #104 from tunapanda/feat/full_experience_opts
Browse files Browse the repository at this point in the history
feat: expose options to enable saving & restoration of user's state
  • Loading branch information
Murage authored Feb 11, 2022
2 parents 14e6a79 + e0988f6 commit 95ff655
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 21 deletions.
77 changes: 57 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Display H5P content without the need for an H5P server
**Source**|**Info**
-----|-----
yarn | `yarn add h5p-standalone`
Release | [Donwload latest version here](https://github.com/tunapanda/h5p-standalone/releases/latest)
Release | [Download latest version here](https://github.com/tunapanda/h5p-standalone/releases/latest)

## Basic Usage
Ensure you have an extracted H5P zip file in your workspace folder first. A simple guide on how to extract an H5P zip file is provided towards the [end section of this guide ](https://github.com/tunapanda/h5p-standalone#extracting-h5p)
Ensure you have an extracted H5P zip file in your workspace folder first. A simple guide on how to extract an H5P zip file is provided [here ](#extracting-h5p)


The player can be set up either by directly calling the already built scripts and styles in your `HTML` page or using `ES6` syntax. For the standalone player to work correctly on a webpage, both the assets, settings, and H5P specific files need to be set properly first.
The player can be set up either by directly calling the already built scripts and styles in your `HTML` page or using `ES6` syntax.

### Direct use
1. Download the project latest release zipped source code from [here](https://github.com/tunapanda/h5p-standalone/releases/latest)
Expand All @@ -39,8 +39,8 @@ The player can be set up either by directly calling the already built scripts an
new H5PStandalone.H5P(el, options);

```
A detailed description of the H5P player arguments are provided under the [advance section](https://github.com/tunapanda/h5p-standalone#advanced-usage)
Simple instruction on how to extract H5P zipped file provided [here](https://github.com/tunapanda/h5p-standalone#extracting-h5p)
A detailed description of the H5P player arguments are provided under the [advance section](#advanced-usage)
Simple instruction on how to extract H5P zipped file provided [here](#extracting-h5p)

### Using ES6
Install the player using yarn
Expand All @@ -66,14 +66,16 @@ const options = {
frameCss: '/assets/styles/h5p.css',
};

new H5P(el, h5pLocation);
new H5P(el, options);
```
A detailed description of the H5P player arguments are provided under the [advance section](https://github.com/tunapanda/h5p-standalone#advanced-usage)
A detailed description of the H5P player arguments are provided under the [advance section](#advanced-usage)
## Advanced Usage
The standalone H5P player constructor accepts two arguments.
1. A HTML element where the H5P iframe will be embedded as the first argument.
2. JSON object with the following options :
### H5P Options
1) Basic options

**Option name**|**Required**|**Description**
-----|-----|----
`h5pJsonPath` | Yes | Path to the H5P content folder
Expand All @@ -82,17 +84,32 @@ The standalone H5P player constructor accepts two arguments.
`id` | No | Player unique identifier. Randomly generated by default
`librariesPath` | No| Path where the player should find the H5P content libraries. Defaults to same as `h5pJsonPath`
`contentJsonPath`|No | Path where the player should find the H5P `content.json` file. Defaults to `{h5pJsonPath}/content/`,
`frame` |No| A boolean on whether to show frame and buttons below H5P
`frame` |No| A boolean on whether to show H5P player frame and buttons
`copyright` |No| A boolean on whether display copyright button
`export` |No| A boolean on whether display a download button.
`icon` |No| A boolean on whether display H5P icon
`downloadUrl` |No| A path or a url that returns zipped h5p for download. The link is used by H5P `export` button
`fullScreen` |No| A boolean on whether to enable fullscreen button if browser supports the feature. Default is `false`|
`xAPIObjectIRI`|No| An identifier for a single unique Activity ~ utilized when generating xAPI [object](https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#acturi) field. Default is page host+pathname
`fullScreen` |No| A boolean on whether to enable the fullscreen button if the browser supports the feature. Default is `false`|
`embed` |No| A boolean on whether display embed button. Default is `false`. N.B. Setting this option to `true` will require an `embedCode` below.
`embedCode` |unless `embed` is true| Embed/Iframe code that user can insert on their site to view same content. Check some caveats to consider [below](#caveats-while-adding-embed-code)
`embedCode` |unless `embed` is true| Embed/Iframe code that user can insert on their site to view the same content. Check some caveats to consider [below](#caveats-while-adding-embed-code)
`customCss` | No | Path(s) to custom stylesheet file(s)
`customJs` | No | Path(s) to custom script file(s)
`xAPIObjectIRI`|No| An identifier for a single unique Activity ~ utilized when generating xAPI [object](https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#acturi) field. Default is page host+pathname

2) User state & data _(kindly refer to [this section](#previous-state-restoration))_

**Option name**|**Required**|**Description**
-----|-----|----
`contentUserData`| No| User previous content interaction state data. The data should be in JSON string format
`saveFreq` |if `contentUserData` or `ajax.*` is set| How often current user engagement content state should be autosaved (in seconds). Default is `false`.
`postUserStatistics` | No | Indicates if H5P should post the results once a finish event is triggered. Default is `false`. **** _Requires `ajax.setFinishedUrl` property to be set_
`ajax` | No | Object required if you need H5P to manage a learner's state
`ajax.setFinishedUrl`| No | Url where H5P should post the results once a finish event is triggered. **** _Requires `postUserStatistics` to be set to true_.
`ajax.contentUserDataUrl`| No | Endpoint where H5P can manage current user state. **** _Requires `user` property to be set_|
`user` | No | Current user data object.
`user.name` | Yes | Used as xAPI actor's name
`user.mail` | Yes | User email. Uniquely identifies the xAPI actor


**Note:**
- One can use absolute URL for `frameCss`, `frameJs`, and for other path options(`h5pJsonPath`,`librariesPath`, & `librariesPath`)
Expand All @@ -104,14 +121,13 @@ The standalone H5P player constructor accepts two arguments.
import { H5P } from 'h5p-standalone';

const el = document.getElementById('h5p-container');
const h5pLocation = './workspace';

const options = {
id: 'exercise-one',
frameJs: './frame.bundle.js',
frameCss: './styles/h5p.css',
h5pJsonPath: '/path/to/h5p-folder',
librariesPath: '/path/to/h5p-folder', //content is on same folder level as h5p.json
contentJsonPath: '/path/to/h5p-folder', //content is on same folder level as h5p.json
librariesPath: '/path/to/shared/libaries', //shared libraries path
frame: true, //required to display copyright, embed, & export buttons
copyright: true,
Expand All @@ -131,8 +147,7 @@ new H5P(el,options)
// do stuff
});

// Or using async-await syntax (async wrapper function removed for readability) :

// Or using the async-await syntax (async wrapper function removed for readability) :
await new H5P(el, options);

```
Expand All @@ -158,9 +173,9 @@ const player1 = new H5P(document.getElementById('h5p-container-1'), player1Optio

player1.then(() => {
return new H5P(document.getElementById('h5p-container-2'), player2Options);
}).then(( => {
}).then(() => {
// do stuff
}));
});


// OR (async wrapper function removed for readability)
Expand All @@ -171,7 +186,7 @@ await new H5P(document.getElementById('h5p-container-2'), player2Options);
```

## Listening to xAPI events
To listen for [xAPI events](https://h5p.org/documentation/api/H5P.XAPIEvent.html) emmitted by the player, you must wait for the player to finish loading and initializing the required content libraries. You can find more info about xAPI events here https://h5p.org/documentation/x-api
To listen for [xAPI events](https://h5p.org/documentation/api/H5P.XAPIEvent.html) emitted by the player, you must wait for the player to finish loading and initializing the required content libraries. You can find more info about xAPI events here https://h5p.org/documentation/x-api
1) Using `then()` method
```js

Expand Down Expand Up @@ -214,16 +229,38 @@ async function myAwesomePlayer() {
myAwesomePlayer();

```

## Previous state restoration.
H5P provides two approaches for restoring a user's previous interaction state:
1) using data provided with `contentUserData` option.
2) automatically fetching the data if `ajax.contentUserDataUrl` is provided

**For both cases, the `saveFreq` option must be set**.

A summary of the previous state restoration process:

1) If the `contentUserData` option is available, skip to the 3rd step.
2) If `contentUserData` is not available but `user.*` and `ajax.contentUserDataUrl` options were provided, request the data from `ajax.contentUserDataUrl` endpoint.
3) Process the previous state `data` as follows:
- where `data[0].state` equals `RESET`, any previous state will be deleted
- else, parse `data[0].state` string and pass it to the H5P player instance.

`ajax.contentUserDataUrl` may include (contentId,dataType,subContentId) placeholders that will be replaced with respective data _automagically_. Placeholders are prefixed with `:`
Placeholders are effective when you need to query only current content user data.

`ajax.contentUserDataUrl` example:
`/api/users/123/h5p/:contentId?data_type=:dataType&subContentId=:subContentId`

### Caveats while adding embed code
- This library includes an H5P resizer by default in `main.bundle.js` at the moment. But, to allow the iframe width to resize promptly, add CSS style setting the width to 100% i.e. `style="width:100%;"`
- If you want to allow users to resize the iframe width and height, set them using placeholders provided by H5P i.e., `width=":w"` and `height=":h"`

Example that combines above points:
An example that combines the above points:

```js
<iframe width=":w" height=":h"
src="https://app.wikonnect.org/embed/JJuzs-OAACU" //replace this with your URL
frameBorder="0" scrolling="no" styles="width:100%"></iframe
frameBorder="0" scrolling="no" styles="width:100%"></iframe>
```


Expand Down
21 changes: 21 additions & 0 deletions cypress/integration/state.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
describe('State management', () => {
beforeEach(() => {
cy.visit('test/state.html');
});

it('should restore previous state with local data', () => {
cy.iframe("#h5p-container-1 iframe.h5p-iframe.h5p-initialized")
.should("be.visible")
.within(() => {
cy.get(".h5p-true-false-answers .h5p-true-false-answer")
.contains("False")
.invoke('attr', 'aria-checked')
.should('eq', 'true'); // no automated selection

//following should pass
cy.get(".h5p-question-check-answer").click();
cy.get(".h5p-joubelui-score-bar-star").should("be.visible");
});
});

});
8 changes: 7 additions & 1 deletion src/js/h5p-integration.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
const integration = {
saveFreq: false,
postUserStatistics: false,
ajax: {
setFinished: undefined,
contentUserData: undefined
},
l10n: {
H5P: {
"fullscreen": "Fullscreen",
Expand Down Expand Up @@ -72,4 +78,4 @@ const integration = {
}
};

window.H5PIntegration = window.H5PIntegration ? {...window.H5PIntegration,...integration} : integration;
window.H5PIntegration = window.H5PIntegration ? { ...window.H5PIntegration, ...integration } : integration;
27 changes: 27 additions & 0 deletions src/js/h5p-standalone.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ export default class H5PStandalone {
customJs: (options.customJs || []).map(script => [urlPath(script)] ),
};

if (options.contentUserData) {
contentOptions.contentUserData = options.contentUserData;
}

/**
* following options overrides global H5PIntegration.
*/
if (options.saveFreq) {
H5PIntegration.saveFreq = options.saveFreq
}

if (options.postUserStatistics) {
H5PIntegration.postUserStatistics = false
}

if (options.ajax && options.ajax.setFinishedUrl) {
H5PIntegration.ajax.setFinished = options.ajax.setFinishedUrl
}

if (options.user) {
H5PIntegration['user'] = options.user;
}

if (options.ajax && options.ajax.contentUserDataUrl) {
H5PIntegration.ajax.contentUserData = options.ajax.contentUserDataUrl
}

this.initElement(el);
return this.initH5P(generalIntegrationOptions, contentOptions, customOptions);
}
Expand Down
25 changes: 25 additions & 0 deletions test/state.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<html lang="en">
<head>
<link type="text/css" rel="stylesheet" media="all" href="/dist/styles/h5p.css"/>
<meta charset="utf-8"/>
<title></title>
<script type="text/javascript" src="/dist/main.bundle.js"></script>
</head>

<body>
<div id="h5p-container-1"></div>

<script type="text/javascript">
new H5PStandalone.H5P(document.getElementById('h5p-container-1'), {
h5pJsonPath: 'full_workspace',
frameJs: '/dist/frame.bundle.js',
frameCss: '/dist/styles/h5p.css',
saveFreq: 3000,
contentUserData: [{
state: '{"answer":false}'
}]
});
</script>
</body>

</html>

0 comments on commit 95ff655

Please sign in to comment.