UNPKG

samagra-x-enketo-core

Version:

Extensible Enketo form engine

254 lines (229 loc) 8.46 kB
/** * @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 };