/**
* @module widgets-controller
*/
import $ from 'jquery';
import _widgets from 'enketo/widgets';
import { elementDataStore as data } from './dom-utils';
import events from './event';
const widgets = _widgets.filter( widget => widget.selector );
let options;
let formHtml;
/**
* Initializes widgets
*
* @static
* @param {jQuery} $group - The element inside which the widgets have to be initialized.
* @param {*} [opts] - Options (e.g. helper function of Form.js passed)
* @return {boolean} `true` when initialized successfuly
*/
function init( $group, opts = {} ) {
if ( !this.form ) {
throw new Error( 'Widgets module not correctly instantiated with form property.' );
}
options = opts;
formHtml = this.form.view.html; // not sure why this is only available in init
const group = $group && $group.length ? $group[ 0 ] : formHtml;
widgets.forEach( Widget => {
_instantiate( Widget, group );
} );
return true;
}
/**
* Enables widgets if they weren't enabled already if they are not readonly.
* In most widgets, this function will do nothing because the disabled attribute was automatically removed from all
* fieldsets, inputs, textareas and selects inside the branch element provided as parameter.
* Note that this function can be called before the widgets have been initialized and will in that case do nothing. This is
* actually preferable than waiting for create() to complete, because enable() will never do anything that isn't
* done during create().
*
* @static
* @param {Element} group - HTML element
*/
function enable( group ) {
widgets.forEach( Widget => {
const els = _getElements( group, Widget.selector )
.filter( el => el.nodeName.toLowerCase() === 'select' ? !el.hasAttribute( 'readonly' ) : !el.readOnly );
new Collection( els ).enable( Widget );
} );
}
/**
* Disables widgets, if they aren't disabled already when the branch was disabled by the controller.
* In most widgets, this function will do nothing because all fieldsets, inputs, textareas and selects will get
* the disabled attribute automatically when the branch element provided as parameter becomes non-relevant.
*
* @static
* @param {Element} group - The element inside which all widgets need to be disabled.
*/
function disable( group ) {
widgets.forEach( Widget => {
const els = _getElements( group, Widget.selector );
new Collection( els ).disable( Widget );
} );
}
/**
* Returns the elements on which to apply the widget
*
* @param {Element} group - A jQuery-wrapped element
* @param {string|null} selector - If the selector is `null`, the form element will be returned
* @return {jQuery} A jQuery collection
*/
function _getElements( group, selector ) {
if ( selector ) {
if ( selector === 'form' ) {
return [ formHtml ];
}
// e.g. if the widget selector starts at .question level (e.g. ".or-appearance-draw input")
if ( group.classList.contains( 'question' ) ) {
return [ ...group.querySelectorAll( 'input:not(.ignore), select:not(.ignore), textarea:not(.ignore)' ) ]
.filter( el => el.matches( selector ) );
}
return [ ...group.querySelectorAll( selector ) ];
}
return [];
}
/**
* Instantiate a widget on a group (whole form or newly cloned repeat)
*
* @param {object} Widget - The widget to instantiate
* @param {Element} group - The element inside which widgets need to be created.
*/
function _instantiate( Widget, group ) {
let opts = {};
if ( !Widget.name ) {
return console.error( 'widget doesn\'t have a name' );
}
if ( Widget.helpersRequired && Widget.helpersRequired.length > 0 ) {
opts.helpers = {};
Widget.helpersRequired.forEach( helper => {
opts.helpers[ helper ] = options[ helper ];
} );
}
const elements = _getElements( group, Widget.selector );
if ( !elements.length ) {
return;
}
new Collection( elements ).instantiate( Widget, opts );
_setLangChangeListener( Widget, elements );
_setOptionChangeListener( Widget, elements );
_setValChangeListener( Widget, elements );
}
/**
* Calls widget('update') when the language changes. This function is called upon initialization,
* and whenever a new repeat is created. In the latter case, since the widget('update') is called upon
* the elements of the repeat, there should be no duplicate eventhandlers.
*
* @param {{name: string}} Widget - The widget configuration object
* @param {Array<Element>} els - Array of elements that the widget has been instantiated on.
*/
function _setLangChangeListener( Widget, els ) {
// call update for all widgets when language changes
if ( els.length > 0 ) {
formHtml.addEventListener( events.ChangeLanguage().type, () => {
new Collection( els ).update( Widget );
} );
}
}
/**
* Calls widget('update') on select-type widgets when the options change. This function is called upon initialization,
* and whenever a new repeat is created. In the latter case, since the widget('update') is called upon
* the elements of the repeat, there should be no duplicate eventhandlers.
*
* @param {{name: string}} Widget - The widget configuration object
* @param {Array<Element>} els - The array of elements that the widget has been instantiated on.
*/
function _setOptionChangeListener( Widget, els ) {
if ( els.length > 0 && Widget.list ) {
$( els ).on( events.ChangeOption().type, function() {
// update (itemselect) picker on which event was triggered because the options changed
new Collection( this ).update( Widget );
} );
}
}
/**
* Calls widget('update') if the form input/select/textarea value changes due to an action outside
* of the widget (e.g. a calculation).
*
* @param {{name: string}} Widget - The widget configuration object.
* @param {Array<Element>} els - The array of elements that the widget has been instantiated on.
*/
function _setValChangeListener( Widget, els ) {
// avoid adding eventhandlers on widgets that apply to the <form> or <label> element
if ( els.length > 0 && els[ 0 ].matches( 'input, select, textarea' ) ) {
els.forEach( el => el.addEventListener( events.InputUpdate().type, event => {
new Collection( event.target ).update( Widget );
} ) );
}
}
class Collection {
/**
* @class
* @param {Array<Element>} elements - HTML elements
*/
constructor( elements ) {
if ( !Array.isArray( elements ) ) {
elements = [ elements ];
}
this.elements = elements;
}
/**
* @param {Element} element - HTML element
* @param {object} Widget - widget to instantiate
* @param {object} [options] - widget options
*/
_instantiateSingleWidget( element, Widget, options = {} ) {
if ( !Widget.condition( element ) ) {
return;
}
if ( data.has( element, Widget ) ) {
return;
}
const w = new Widget( element, options );
if ( w instanceof Promise ) {
w.then( wr => data.put( element, Widget.name, wr ) );
} else {
data.put( element, Widget.name, w );
}
}
/**
* @param {object} Widget - widget to instantiate
* @param {Function} method - widget function to call
*/
_methodCall( Widget, method ) {
this.elements.forEach( element => {
const w = data.get( element, Widget.name );
if ( w ) {
w[ method ]();
}
} );
}
/**
* @param {object} Widget - widget to instantiate
* @param {object} [options] - widget options
*/
instantiate( Widget, options ) {
this.elements.forEach( el => this._instantiateSingleWidget( el, Widget, options ) );
}
/**
* @param {object} Widget - widget to instantiate
*/
update( Widget ) {
this._methodCall( Widget, 'update' );
}
/**
* @param {object} Widget - widget to instantiate
*/
disable( Widget ) {
this._methodCall( Widget, 'disable' );
}
/**
* @param {object} Widget - The widget to instantiate
*/
enable( Widget ) {
this._methodCall( Widget, 'enable' );
}
}
export default {
init,
enable,
disable
};