Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

enketo-core

Package Overview
Dependencies
Maintainers
3
Versions
300
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

enketo-core - npm Package Compare versions

Comparing version 7.2.1 to 7.2.2

2

package.json

@@ -5,3 +5,3 @@ {

"homepage": "https://enketo.org",
"version": "7.2.1",
"version": "7.2.2",
"license": "Apache-2.0",

@@ -8,0 +8,0 @@ "os": [

@@ -11,8 +11,4 @@ [![npm version](https://badge.fury.io/js/enketo-core.svg)](http://badge.fury.io/js/enketo-core) ![Build Status](https://github.com/enketo/enketo-core/actions/workflows/npmjs.yml/badge.svg)

**To get started visit our [technical documentation](https://github.com/enketo/enketo-core/tree/master/tutorials).**
## Browser support
Follow the [Enketo blog](https://blog.enketo.org) or [Enketo on twitter](https://twitter.com/enketo) to stay up to date.
### Browser support
The following browsers are officially supported:

@@ -29,4 +25,397 @@

### Releases
### Performance (live)
See [graphs](https://github.com/enketo/enketo-core-performance-monitor#live-results)
## Usage as a library
1. Install with `npm install enketo-core --save` or include as a git submodule.
2. Develop a way to perform an [XSL Transformation](https://enketo.org/develop/#transformation) on OpenRosa-flavoured XForms inside your app. The transformation will output an XML instance and a HTML form. See [enketo-transformer](https://github.com/enketo/enketo-transformer) for an available library/app to use or develop your own.
3. Add [themes](./src/sass) to your stylesheet build system (2 stylesheets per theme, 1 is for `media="print"`).
4. Override [the files under "browser"](./package.json), e.g. using [aliasify](https://www.npmjs.com/package/aliasify) with your app-specific versions.
5. Main methods illustrated in code below:
```javascript
// assumes the enketo-core package is mapped from the node_modules folder
import { Form } from 'enketo-core';
// The XSL transformation result contains a HTML Form and XML instance.
// These can be obtained dynamically on the client, or at the server/
// In this example we assume the HTML was injected at the server and modelStr
// was injected as a global variable inside a <script> tag.
// required HTML Form DOM element
const formEl = document.querySelector('form.or');
// required object containing data for the form
const data = {
// required string of the default instance defined in the XForm
modelStr: globalXMLInstance,
// optional string of an existing instance to be edited
instanceStr: null,
// optional boolean whether this instance has ever been submitted before
submitted: false,
// optional array of external data objects containing:
// {id: 'someInstanceId', xml: XMLDocument}
external: [],
// optional object of session properties
// 'deviceid', 'username', 'email', 'phonenumber', 'simserial', 'subscriberid'
session: {},
};
// Form-specific configuration
const options = {};
// Instantiate a form, with 2 parameters
const form = new Form(formEl, data, options);
// Initialize the form and capture any load errors
let loadErrors = form.init();
// If desired, scroll to a specific question with any XPath location expression,
// and aggregate any loadErrors.
loadErrors = loadErrors.concat(form.goTo('//repeat[3]/node'));
// submit button handler for validate button
$('#submit').on('click', function () {
// clear non-relevant questions and validate
form.validate().then(function (valid) {
if (!valid) {
alert('Form contains errors. Please see fields marked in red.');
} else {
// Record is valid!
const record = form.getDataStr();
// reset the form view
form.resetView();
// reinstantiate a new form with the default model and no options
form = new Form(formSelector, { modelStr: modelStr }, {});
// do what you want with the record
}
});
});
```
## Global Configuration
Global configuration (per app) is done in [config.json](./config.json) which is meant to be overridden by a config file in your own application (e.g. by using rollup).
### maps
The `maps` configuration can include an array of Mapbox TileJSON objects (or a subset of these with at least a `name`, `tiles` (array) and an `attribution` property, and optionally `maxzoom` and `minzoom`). You can also mix and match Google Maps layers. Below is an example of a mix of two map layers provided by OSM (in TileJSON format) and Google maps.
```
[
{
"name": "street",
"tiles": [ "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" ],
"attribution": "Map data © <a href=\"http://openstreetmap.org\">OpenStreetMap</a> contributors"
},
{
"name": "satellite",
"tiles": "GOOGLE_SATELLITE"
}
]
```
For GMaps layers you have the four options as tiles values: `"GOOGLE_SATELLITE"`, `"GOOGLE_ROADMAP"`, `"GOOGLE_HYBRID"`, `"GOOGLE_TERRAIN"`. You can also add other TileJSON properties, such as minZoom, maxZoom, id to all layers.
### googleApiKey
The Google API key that is used for geocoding (in the geo widgets' search box). Can be obtained [here](https://console.developers.google.com/project). Make sure to enable the _GeoCoding API_ service. If you are using Google Maps layers, the same API key is used. Make sure to enable the _Google Maps JavaScript API v3_ service as well in that case (see next item).
### validateContinuously
This setting with the default `false` value determines whether Enketo should validate questions immediately if a related value changes. E.g. if question A has a constraint that depends on question B, this mode would re-validate question A if the value for question B changes. **This mode will slow down form traversal.** When set to `false` that type of validation is only done at the end when the Submit button is clicked or in Pages mode when the user clicks Next.
### validatePage
This setting with default `true` value determines whether the Next button should trigger validation of the current page and block the user from moving to the next page if validation fails.
### swipePage
This setting with default `true` value determines whether to enable support for _swiping_ to the next and previous page for forms that are divided into pages.
## Form Configuration
Per-form configuration is done by adding an (optional) options object as 3rd parameter when instantiating a form.
### Print only the "relevant" parts of the form
If `printRelevantOnly` is set to `true` or not set at all, printing the form only includes what is visible, ie. all the groups and questions that do not have a `relevant` expression or for which the expression evaluates to `true`.
```
new Form(formselector, data, {
printRelevantOnly: false
});
```
### Explicitly set the default form language
The `language` option overrides the default languages rules of the XForm itself. Pass any valid and present-in-the-form IANA subtag string, e.g. `ar`.
## How to develop Enketo Core
1. install prerequisites:
- Volta (optional, but recommended)
- Node.js 16 and npm 6 (Node.js 14 is also supported)
- [grunt-cli](https://gruntjs.com/getting-started)
2. install dependencies with `npm install`
3. build with `grunt` (`npx grunt`)
4. start built-in auto-reloading development server with `npm start`
5. browse to [http://localhost:8005](http://localhost:8005/) and load an XForm url with the `xform` queryparameter or load a local from the /tests/forms folder in this repo
6. run tests with `npm test` (headless chrome) and `npm run test-browsers` (browsers); **note:** running tests updates the coverage badge in README.md, but these changes should not be committed except when preparing a release
7. adding the querystring `touch=true` and reducing the window size allows you to simulate mobile touchscreens
### Notes for JavaScript Developers
- When creating new functions/Classes, make sure to describe them with JSDoc comments.
- JavaScript style see [ESLint](./eslintrc.json) config files. The check is added to the grunt `test` task. You can also manually run `grunt eslint:fix` to fix style issues.
- Testing is done with Mocha and Karma (all: `grunt karma`, headless: `grunt karma:headless`, browsers: `grunt karma:browsers`)
- Tests can be run in watch mode for [TDD](https://en.wikipedia.org/wiki/Test-driven_development) workflows with `npm run test-watch`, and support for debugging in [VSCode](https://code.visualstudio.com/) is provided. For instructions see [Debugging test watch mode in VSCode](./#debugging-test-watch-mode-in-vscode) below
- When making a pull request, please add tests where relevant
#### Debugging test watch mode in VSCode
Basic usage:
1. Go to VSCode's "Run and Debug" panel
2. Select "Test (watch + debug)"
3. Click the play button
Optionally, you can add a keyboard shortcut to select launch tasks:
1. Open the keyboard shortcuts settings (cmd+k cmd+s on Mac, ctrl+k ctrl+s on other OSes)
2. Search for `workbench.action.debug.selectandstart`
3. Click the + button to add your preferred keybinding keybinding
### Notes for CSS Developers
The core can be fairly easily extended with alternative themes.
See the _plain_, the _grid_, and the _formhub_ themes already included in [/src/sass](./src/sass).
We would be happy to discuss whether your contribution should be a part of the core, the default theme or be turned into a new theme.
For custom themes that go beyond just changing colors and fonts, keep in mind all the different contexts for a theme:
1. non-touchscreen vs touchscreen (add ?touch=true during development)
2. default one-page-mode and multiple-pages-mode
3. right-to-left form language vs left-to-right form language (!) - also the UI-language may have a different directionality
4. screen view vs. print view
5. questions inside a (nested) repeat group have a different background
6. large screen size --> smaller screen size ---> smallest screen size
7. question in valid vs. invalid state
### Widgets in Enketo Core
Widgets extend the [Widget class](https://github.com/enketo/enketo-core/blob/master/src/js/widget.js). This is an example:
(see full functioning example at [/src/widget/example/my-widget.js](https://github.com/enketo/enketo-core/blob/master/src/widget/example/my-widget.js)
```js
import Widget from '../../js/widget';
/*
* Make sure to give the widget a unique widget class name and extend Widget.
*/
class MyWidget extends Widget {
/*
* The selector that determines on which form control the widget is instantiated.
* Make sure that any other widgets that target the same from control are not interfering with this widget by disabling
* the other widget or making them complementary.
* This function is always required.
*/
static get selector() {
return '.or-appearance-my-widget input[type="number"]';
}
/*
* Initialize the widget that has been instantiated using the Widget (super) constructor.
* The _init function is called by that super constructor unless that constructor is overridden.
* This function is always required.
*/
_init() {
// Hide the original input
this.element.classList.add('hide');
// Create the widget's DOM fragment.
const fragment = document.createRange().createContextualFragment(
`<div class="widget">
<input class="ignore" type="range" min="0" max="100" step="1"/>
<div>`
);
fragment.querySelector('.widget').appendChild(this.resetButtonHtml);
// Only when the new DOM has been fully created as a HTML fragment, we append it.
this.element.after(fragment);
const widget = this.element.parentElement.querySelector('.widget');
this.range = widget.querySelector('input');
// Set the current loaded value into the widget
this.value = this.originalInputValue;
// Set event handlers for the widget
this.range.addEventListener('change', this._change.bind(this));
widget
.querySelector('.btn-reset')
.addEventListener('click', this._reset.bind(this));
// This widget initializes synchronously so we don't return anything.
// If the widget initializes asynchronously return a promise that resolves to `this`.
}
_reset() {
this.value = '';
this.originalInputValue = '';
this.element.classList.add('empty');
}
_change(ev) {
// propagate value changes to original input and make sure a change event is fired
this.originalInputValue = ev.target.value;
this.element.classList.remove('empty');
}
/*
* Disallow user input into widget by making it readonly.
*/
disable() {
this.range.disabled = true;
}
/*
* Performs opposite action of disable() function.
*/
enable() {
this.range.disabled = false;
}
/*
* Update the language, list of options and value of the widget.
*/
update() {
this.value = this.originalInputValue;
}
/*
* Obtain the current value from the widget. Usually required.
*/
get value() {
return this.element.classList.contains('empty') ? '' : this.range.value;
}
/*
* Set a value in the widget. Usually required.
*/
set value(value) {
this.range.value = value;
}
}
export default MyWidget;
```
Some of the tests are common to all widgets, and can be run with a few lines:
(see full functioning example at [/test/spec/widget.example.spec.js](https://github.com/enketo/enketo-core/blob/master/test/spec/widget.example.spec.js))
```js
import ExampleWidget from '../../src/widget/example/my-widget';
import { runAllCommonWidgetTests } from '../helpers/testWidget';
const FORM = `<label class="question or-appearance-my-widget">
<input type="number" name="/data/node">
</label>`;
const VALUE = '2';
runAllCommonWidgetTests(ExampleWidget, FORM, VALUE);
```
### DO
- use the rank widget as a more complex example that uses the best practices (some other widgets use an older style)
- add an `_init` function to your widget that either returns nothing or a Promise (if it initializes asynchronously)
- include a widget.my-widget.spec.js file in the /test folder
- run at least the standardized common widget tests by doing: TBD
- make the widget responsive up to a minimum window width of 320px
- ensure the widget's scss and js file is/are loaded in widgets.js and \_widgets.scss respectively
- if hiding the original input element, it needs to load the default value `this.originalInputValue` into the widget
- if hiding the original input element, keep its value syncronized using `this.originalInputValue = ...`
- if hiding the original input element, it needs to listen for the `applyfocus` event on the original input and focus the widget
- if hiding the original input element, the widget value needs to update when the original input updates due to a calculation or becoming non-relevant (update)
- apply the `widget` css class to the top level elements it adds to the DOM (but not to their children)
- new input/select/textarea elements inside widgets should have the `ignore` class to isolate them from the Enketo form engine
- include `enable()`, `disable()` and `update()` method overrides. See the Widget class.
- if the widget needs tweaks or needs to be disabled for mobile use, use support.js to detect this and override the static `condition()` function in Widget.js.
- allow clearing of the original input (i.e. setting value to '')
- if the widget does not get automatic (built-in HTML) focus, trigger a `fakefocus` event to the original input when the widget gets focus (rarely required, but see rank widget)
#### DON'T
- do not include jQuery, React, Vue or any other general purpose libraries or frameworks
### Events in Enketo Core
##### inputupdate
Fired on a form control when it is programmatically updated and when this results in a change in value
#### xforms-value-changed
Fired on a form control when it is updated directly by the user and when this results in a change in value
##### invalidated
Fired on a form control when it has failed constraint, datatype, or required validation.
##### dataupdate
Fired on model.$events, when a single model value has changed its value, a repeat is added, or a node is removed. It passes an "update object". This event is propagated for external use by firing it on the form.or element as well.
##### odk-instance-first-load
Fired on model.events when a new record (instance) is loaded for the first time. It's described here: [odk-instance-first-load](https://getodk.github.io/xforms-spec/#event:odk-instance-first-load).
##### odk-new-repeat
Fired on a newly added repeat. It's described here: [odk-instance-first-load](https://getodk.github.io/xforms-spec/#event:odk-new-repeat).
##### removerepeat
Fired on the repeat or repeat element immediately following a removed repeat.
##### removed
Fired on model.events, when a node is removed. It passes an "update object". This event is propagated for external use by firing it on the form.or element as well.
##### goto-irrelevant
Fired on form control when an attempt is made to 'go to' this field but it is hidden from view because it is non-relevant.
##### goto-invisible
Fired on form control when an attempt is made to 'go to' this field but it is hidden from view because it is has no form control.
##### pageflip
Fired when user flips to a new page, on the page element itself.
##### edited
Fired on form.or element when user makes first edit in form. Fires only once.
##### validation-complete
Fired on form.or element when validation completes.
##### progress-update
Fired when the user moves to a different question in the form.
## Release
1. Create release PR

@@ -65,8 +454,4 @@ 1. Update `CHANGELOG.md`

### Performance (live)
## License
See [graphs](https://github.com/enketo/enketo-core-performance-monitor#live-results)
### License
See [license](https://github.com/enketo/enketo-core/blob/master/LICENSE) document and additional clause below:

@@ -73,0 +458,0 @@

@@ -360,9 +360,19 @@ import $ from 'jquery';

let fileOrURL = file;
if (typeof file === 'string') {
try {
fileOrURL = await fileManager.getFileUrl(file);
} catch {
// Ignore error, attempt to load
}
}
try {
if (
typeof file === 'string' &&
file.startsWith('jr://') &&
typeof fileOrURL === 'string' &&
fileOrURL.startsWith('jr://') &&
this.element.dataset.loadedUrl
) {
file = this.element.dataset.loadedUrl;
fileOrURL = this.element.dataset.loadedUrl;
}

@@ -372,3 +382,3 @@

await this.pad.setBaseImage(file);
await this.pad.setBaseImage(fileOrURL);

@@ -375,0 +385,0 @@ return this.pad.toObjectURL();

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc