Source: forms/form-field.js


import $ from '$qui/lib/jquery.module.js'

import {AssertionError}  from '$qui/base/errors.js'
import {gettext}         from '$qui/base/i18n.js'
import {mix}             from '$qui/base/mixwith.js'
import Icon              from '$qui/icons/icon.js'
import StockIcon         from '$qui/icons/stock-icon.js'
import * as Theme        from '$qui/theme.js'
import {asap}            from '$qui/utils/misc.js'
import VisibilityManager from '$qui/utils/visibility-manager.js'
import ViewMixin         from '$qui/views/view.js'
import {STATE_NORMAL}    from '$qui/views/view.js'
import * as Window       from '$qui/window.js'

import {STATE_APPLIED}   from './forms.js'
import {ValidationError} from './forms.js'


/**
 * A form field.
 * @alias qui.forms.FormField
 * @mixes qui.views.ViewMixin
 */
class FormField extends mix().with(ViewMixin) {

    /**
     * @constructs
     * @param {String} name field name
     * @param {String} [initialValue] an optional initial value for the field
     * @param {String} [label] field label
     * @param {String} [description] an optional field description
     * @param {String} [unit] an optional unit of measurement
     * @param {Boolean} [separator] if set to `true`, a separator will be drawn above the field (defaults to `false`)
     * @param {Boolean} [required] if set to `true`, the field value must be present for a successful validation
     * (defaults to `false`)
     * @param {Boolean} [readonly] if set to `true`, the field widget will not allow user input
     * @param {Boolean} [disabled] if set to `true`, the field widget will be completely disabled (defaults to `false`)
     * @param {Boolean} [hidden] if set to `true`, the field will be initially hidden (defaults to `false`)
     * @param {Boolean} [forceOneLine] if set to `true`, label and value will be shown on one single line, ignoring
     * form's `compact` attribute
     * @param {?Number} [valueWidth] sets the width of the value column, in percents, relative to the form body; if
     * set to `null` (the default), the form's `valuesWidth` setting will be used; if set to `0`, no width constraints
     * will be imposed on value or label; this attribute only works for fields displayed on a single line
     * @param {Function} [onChange] called whenever the field value changes (see {@link qui.forms.FormField#onChange})
     * @param {Function} [validate] a validator function (see {@link qui.forms.FormField#validate})
     * @param {...*} args parent class parameters
     */
    constructor({
        name,
        initialValue = null,
        label = '',
        description = '',
        unit = '',
        separator = false,
        required = false,
        readonly = false,
        disabled = false,
        hidden = false,
        forceOneLine = false,
        valueWidth = null,
        onChange = null,
        validate = null,
        ...args
    }) {
        super(args)

        this._name = name
        this._initialValue = initialValue
        this._label = label
        this._description = description
        this._unit = unit
        this._separator = separator
        this._required = required
        this._readonly = readonly
        this._disabled = disabled
        this._initiallyHidden = hidden
        this._visibilityManager = null
        this._forceOneLine = forceOneLine
        this._valueWidth = valueWidth

        if (onChange) {
            this.onChange = onChange
        }
        if (validate) {
            this.validate = validate
        }

        this._form = null
        this._widget = null
        this._descriptionDiv = null
        this._warningDiv = null
        this._errorDiv = null
        this._labelDiv = null
        this._valueDiv = null
        this._sideIcon = null
        this._sideIconDiv = null
        this._focused = false
        this._origValue = null
        this._changed = false
    }

    makeHTML() {
        if (!this._form) {
            throw new AssertionError('makeHTML() called before assigning to a form')
        }

        let html = $('<div></div>', {'class': 'qui-form-field', 'data-name': this._name})

        if (this._forceOneLine) {
            html.addClass('force-one-line')
        }

        let valueWidth = this._valueWidth
        if (valueWidth == null) {
            if (this._label) {
                valueWidth = this._form._valuesWidth
            }
            else {
                valueWidth = 0
            }
        }

        let useOneLine = !this._form.isCompact() || this._forceOneLine

        /* Description */
        this._descriptionDiv = this.makeDescriptionHTML()
        html.append(this._descriptionDiv)
        if (this._description) {
            html.addClass('has-description')
        }

        /* Warning */
        this._warningDiv = this.makeWarningHTML()
        html.append(this._warningDiv)

        /* Error */
        this._errorDiv = this.makeErrorHTML()
        html.append(this._errorDiv)

        /* Label */
        this._labelDiv = this.makeLabelHTML()
        html.append(this._labelDiv)
        if (useOneLine) {
            if (valueWidth) {
                this._labelDiv.css('width', `${(100 - valueWidth)}%`)
            }
            else {
                html.addClass('auto-width')
            }
        }

        if (this._label) {
            this._labelDiv.children('span.qui-form-field-label-caption').html(this._label)
            html.addClass('has-label')
        }

        /* Unit */
        if (this._unit) {
            this._labelDiv.find('span.qui-form-field-unit').text(`(${this._unit})`)
            html.addClass('has-unit')
        }

        /* Value */
        this._valueDiv = this.makeValueHTML()
        html.append(this._valueDiv)
        if (valueWidth && useOneLine) {
            this._valueDiv.css('width', `${valueWidth}%`)
        }

        /* Side icon */
        this._sideIconDiv = this.makeSideIconHTML()
        html.append(this._sideIconDiv)

        /* Other attributes */
        if (this._required) {
            html.addClass('required')
        }

        if (this._readonly) {
            html.addClass('readonly')
            this.setWidgetReadonly(true)
        }

        if (this._disabled) {
            this.disableWidget()
        }

        if (this._separator) {
            html.addClass('separator')
        }

        /* Initial value */
        if (this._initialValue != null) {
            this.setValue(this._initialValue)
        }

        this._visibilityManager = new VisibilityManager({element: html})

        if (this._initiallyHidden) {
            this._visibilityManager.hideElement()
        }

        return html
    }

    /**
     * Create the description HTML element.
     * @returns {jQuery}
     */
    makeDescriptionHTML() {
        let descDiv = $('<div></div>', {class: 'qui-form-field-description'})
        let descLabel = $('<div></div>', {class: 'qui-form-field-description-label'})
        let descIcon = $('<span></span>', {class: 'qui-form-field-description-icon'})
        let descText = $('<span></span>', {class: 'qui-form-field-description-text'})

        descLabel.append(descIcon).append(descText)
        descDiv.append(descLabel)

        if (this._description) {
            descText.html(this._description)
        }

        return descDiv
    }

    /**
     * Create the warning HTML element.
     * @returns {jQuery}
     */
    makeWarningHTML() {
        let warningDiv = $('<div></div>', {class: 'qui-form-field-warning'})
        let warningLabel = $('<div></div>', {class: 'qui-form-field-warning-label'})
        let warningText = $('<span></span>', {class: 'qui-form-field-warning-text'})

        warningLabel.append(warningText)
        warningDiv.append(warningLabel)

        return warningDiv
    }

    /**
     * Create the error HTML element.
     * @returns {jQuery}
     */
    makeErrorHTML() {
        let errorDiv = $('<div></div>', {class: 'qui-form-field-error'})
        let errorLabel = $('<div></div>', {class: 'qui-form-field-error-label'})
        let errorText = $('<span></span>', {class: 'qui-form-field-error-text'})

        errorLabel.append(errorText)
        errorDiv.append(errorLabel)

        return errorDiv
    }

    /**
     * Create the label HTML element.
     * @returns {jQuery}
     */
    makeLabelHTML() {
        let labelDiv = $('<div></div>', {class: 'qui-form-field-label'})

        let captionSpan = $('<span></span>', {class: 'qui-form-field-label-caption'})

        let descriptionIcon = new StockIcon({
            name: 'qmark',
            variant: 'disabled',
            scale: 0.5
        })

        let unitSpan = $('<span></span>', {class: 'qui-form-field-unit'})

        let descriptionIconDiv = $('<div></div>', {class: 'qui-base-button qui-form-field-description-icon'})
        descriptionIcon.applyTo(descriptionIconDiv)

        descriptionIconDiv.on('click', function () {
            if (this._description) {
                this.setDescriptionVisible(!this.isDescriptionVisible())
            }
        }.bind(this))

        labelDiv.append(captionSpan)
        labelDiv.append(unitSpan)
        labelDiv.append(descriptionIconDiv)

        return labelDiv
    }

    /**
     * Create the value HTML element.
     * @returns {jQuery}
     */
    makeValueHTML() {
        let valueDiv = $('<div></div>', {class: 'qui-form-field-value'})

        /* Value widget */
        let widget = this.getWidget()

        widget.on('change', () => this.handleChange(this.getValue()))
        widget.on('focus', () => this.handleFocus())
        widget.on('blur', () => this.handleBlur())

        /* Add widget to value div, but only if it hasn't already been added to another container; this allows fields
         * like CompositeField to use the widget directly inside their container. */
        if (!widget.parent().length) {
            valueDiv.append(widget)
        }

        return valueDiv
    }

    /**
     * Create the side icon HTML element.
     * @returns {jQuery}
     */
    makeSideIconHTML() {
        let sideIconDiv = $('<div></div>', {class: 'qui-base-button qui-form-field-side-icon'})

        this._sideIcon = new StockIcon({name: 'success', scale: 0.75})
        this._sideIcon.applyTo(sideIconDiv)

        return sideIconDiv
    }


    /* Value */

    /**
     * Return the current field value.
     * @returns {*}
     */
    getValue() {
        return this.widgetToValue()
    }

    /**
     * Update the current field value.
     * @param {*} value
     */
    setValue(value) {
        this._origValue = value
        this._changed = false

        this.valueToWidget(value)
    }

    /**
     * Handle change events.
     * @param {*} value new value
     */
    handleChange(value) {
        let form = this.getForm()

        this.clearApplied()
        this._changed = true
        this.onChange(value, form)
        form._handleFieldChange(this)
    }

    /**
     * Tell if field has been changed since the last time it was applied.
     * @returns {boolean}
     */
    isChanged() {
        return this._changed
    }

    /**
     * Clear the changed flag.
     */
    clearChanged() {
        this._origValue = this.getValue()
        this._changed = false
    }

    /**
     * Return the original field value (before user changes). The original value is always the last value that has been
     * applied.
     * @returns {*}
     */
    getOrigValue() {
        return this._origValue
    }


    /* Name */

    /**
     * Return the field name.
     * @returns {String}
     */
    getName() {
        return this._name
    }


    /* Label */

    /**
     * Return the field label.
     * @returns {String}
     */
    getLabel() {
        return this._label
    }

    /**
     * Update the field label.
     * @param {String} label
     */
    setLabel(label) {
        this._label = label

        if (!this.hasHTML()) {
            return
        }

        this._labelDiv.children('div.qui-form-field-label-caption').html(label || '')
        this.getHTML().toggleClass('has-label', Boolean(label))
    }


    /* Description */

    /**
     * Return the field description.
     * @returns {String}
     */
    getDescription() {
        return this._description
    }

    /**
     * Update the field description.
     * @param {String} description
     */
    setDescription(description) {
        this._description = description

        if (!this.hasHTML()) {
            return
        }

        let descText = this.getHTML().find('div.qui-form-field-description div.qui-form-field-description-text')

        if (description) {
            descText.html(description)
            this.getHTML().addClass('has-description')
        }
        else {
            descText.html('')
            this.getHTML().removeClass('has-description description-visible')
        }
    }

    /**
     * Tell if the field description is visible or not.
     * @returns {Boolean}
     */
    isDescriptionVisible() {
        return this.getHTML().hasClass('description-visible')
    }

    setDescriptionVisible(visible) {
        if (visible) {
            this.getHTML().addClass('description-visible')

            let field = this
            let descDiv = this.getHTML().children('div.qui-form-field-description')

            /* Close description when clicking anywhere */
            Window.$body.on('click', function f(e) {
                if (field.getHTML() === e.target ||
                    field.getHTML().has(e.target).length ||
                    descDiv.has(e.target).length) {

                    /* Clicked on the description itself */
                    return
                }

                Window.$body.off('click', f)
                field.setDescriptionVisible(false)
            })
        }
        else {
            this.getHTML().removeClass('description-visible')
        }
    }


    /* Unit */

    /**
     * Return the field unit.
     * @returns {String}
     */
    getUnit() {
        return this._unit
    }

    /**
     * Update the field unit.
     * @param {String} unit
     */
    setUnit(unit) {
        this._unit = unit

        if (!this.hasHTML()) {
            return
        }

        this._labelDiv.find('span.qui-form-field-unit').text(`(${this._unit || ''})`)
        this.getHTML().toggleClass('has-unit', Boolean(unit))
    }


    /* Separator */

    /**
     * Tell if the field has separator above.
     * @returns {Boolean}
     */
    hasSeparator() {
        return this._separator
    }

    /**
     * Set the field separator.
     * @param {Boolean} separator
     */
    setSeparator(separator) {
        this._separator = separator

        if (!this.hasHTML()) {
            return
        }

        this.getHTML().toggleClass('separator', separator)
    }


    /* Required */

    /**
     * Tell if the field is required.
     * @returns {Boolean}
     */
    isRequired() {
        return this._required
    }

    /**
     * Update the required state.
     * @param {Boolean} required
     */
    setRequired(required) {
        this._required = required

        if (!this.hasHTML()) {
            return
        }

        this.getHTML().toggleClass('required', required)
    }


    /* Read-only state */

    /**
     * Tell if the field is read-only.
     * @returns {Boolean}
     */
    isReadonly() {
        return this._readonly
    }

    /**
     * Update the read-only state.
     * @param {Boolean} readonly
     */
    setReadonly(readonly) {
        this._readonly = readonly
        this.setWidgetReadonly(readonly)

        if (!this.hasHTML()) {
            return
        }

        this.getHTML().toggleClass('readonly', readonly)
    }


    /* Disabled state */

    /**
     * Tell if the field is disabled.
     * @returns {Boolean}
     */
    isDisabled() {
        return this._disabled
    }

    /**
     * Enable the field.
     */
    enable() {
        if (!this._disabled) {
            return
        }

        this._disabled = false

        this.enableWidget()
    }

    /**
     * Disable the field.
     */
    disable() {
        if (this._disabled) {
            return
        }

        this._disabled = true

        this.disableWidget()
    }


    /* Visibility */

    /**
     * Tell if the field is hidden.
     * @returns {Boolean}
     */
    isHidden() {
        return !this._visibilityManager.isElementVisible()
    }

    /**
     * Show the field.
     */
    show() {
        if (this._visibilityManager.isElementVisible()) {
            return
        }

        this._visibilityManager.showElement()

        if (this._form._continuousValidation) {
            /* We need to revalidate the form upon field show, since hidden fields are considered always valid */
            this._form._clearValidationCache(this._name)
            this._form.updateValidationStateASAP()
        }

        this._form.updateFieldsState()
    }

    /**
     * Hide the field.
     */
    hide() {
        if (!this._visibilityManager.isElementVisible()) {
            return
        }

        this._visibilityManager.hideElement()

        if (this._form._continuousValidation) {
            /* We need to revalidate the form upon field hide, since hidden fields are considered always valid */
            this._form._clearValidationCache(this._name)
            this._form.updateValidationStateASAP()
        }

        this._form.updateFieldsState()
    }


    /* Focus */

    /**
     * Called whenever the field is focused.
     */
    onFocus() {
    }

    /**
     * Called whenever the field loses focus.
     */
    onBlur(value) {
    }

    /**
     * Tell if the field is focused or not.
     * @returns {Boolean}
     */
    isFocused() {
        return this._focused
    }

    /**
     * Focus the field.
     */
    focus() {
        this.getWidget().focus()
    }

    /**
     * Handle focus events.
     */
    handleFocus() {
        this._focused = true
        this.onFocus()
    }

    /**
     * Handle blur events.
     */
    handleBlur() {
        this._focused = false
        this.onBlur()
    }


    /* Progress state */

    showProgress(percent) {
        this.setSideIcon('progress')
    }

    hideProgress() {
        this._updateSideIcon()
    }


    /* Warning state */

    showWarning(message) {
        this.getHTML().find('span.qui-form-field-warning-text').html(message)
        this.getHTML().addClass('has-warning')
        if (message) {
            this.getHTML().addClass('has-warning-message')
        }

        this._updateSideIcon()
    }

    hideWarning() {
        this.getHTML().removeClass('has-warning has-warning-message')
        Theme.afterTransition(function () {
            if (this.hasWarning()) {
                return /* Warning has been reshown */
            }

            this.getHTML().find('span.qui-form-field-warning-text').html('')
        }.bind(this), this.getHTML())

        this._updateSideIcon()
    }


    /* Error state */

    showError(message) {
        this.getHTML().find('span.qui-form-field-error-text').html(message)
        this.getHTML().addClass('has-error')
        if (message) {
            this.getHTML().addClass('has-error-message')
        }

        this._updateSideIcon()
    }

    hideError() {
        this.getHTML().removeClass('has-error has-error-message')
        Theme.afterTransition(function () {
            if (this.hasError()) {
                return /* Error has been reshown */
            }

            this.getHTML().find('span.qui-form-field-error-text').html('')
        }.bind(this), this.getHTML())

        this._updateSideIcon()
    }


    /* Applied state */

    /**
     * Optionally put the field in the applied state {@link qui.forms.STATE_APPLIED}.
     */
    setApplied() {
        this._origValue = this.getValue()
        this._changed = false
        this.setState(STATE_APPLIED)
    }

    /**
     * Put the field in the normal state, but only if the current state is {@link qui.forms.STATE_APPLIED}.
     */
    clearApplied() {
        if (this.getState() === STATE_APPLIED) {
            this.setState(STATE_NORMAL)
        }
    }

    enterState(oldState, newState) {
        switch (newState) {
            case STATE_APPLIED:
                this._updateSideIcon()
                break

            default:
                super.enterState(oldState, newState)
        }
    }

    /**
     * Tell if the field data has been applied (and thus there are no pending changes).
     * @returns {Boolean}
     */
    isApplied() {
        return this.getState() === STATE_APPLIED
    }


    /* Value */

    /**
     * Called whenever the field value is changed by the user.
     * @param {*} value the new (unvalidated) field value
     * @param {qui.forms.Form} form the owning form
     */
    onChange(value, form) {
    }

    /**
     * Tell if a given value is valid or not.
     *
     * Override this method to implement custom validation for this field.
     *
     * @param {*} value the value to validate
     * @param {Object} data the form data
     * @returns {?Promise<*,qui.forms.ValidationError>} `null` or a promise that resolves, if the value is valid; a
     * promise that rejects with a validation error otherwise
     * @throws qui.forms.ValidationError the validation error can also be thrown instead of being returned in a rejected
     * response
     */
    validate(value, data) {
        return null
    }

    /**
     * Wrap validate() into a try/catch and return a validation promise. Also apply required and widget validation.
     * @private
     * @param {*} value the value to validate
     * @param {Object} data
     * @returns {Promise}
     */
    _validate(value, data) {
        /* Required validation */
        if (this._required && !this.isHidden()) {
            if (value == null || value === false || value === '') {
                return Promise.reject(new ValidationError(gettext('This field is required.')))
            }
        }

        /* Widget validation */
        let error = this.validateWidget(value)
        if (error) {
            return Promise.reject(new ValidationError(error))
        }

        /* Custom validation */
        try {
            return this.validate(value, data) || Promise.resolve()
        }
        catch (e) {
            return Promise.reject(e)
        }
    }


    /* Side icon */

    /**
     * Show or hide the side icon, updating it according to the supplied icon.
     * @param {?String|qui.icons.Icon} icon an icon or an icon type; passing `null` hides the side icon; known icon
     * types are:
     *  * `"success"`
     *  * `"warning"`
     *  * `"error"`
     *  * `"progress"`
     * @param {Function} [clickCallback] an optional function to be called when the icon is clicked; the function will
     * be called with the field as `this` argument
     */
    setSideIcon(icon, clickCallback) {
        /* Make sure the HTML is created */
        this.getHTML()

        if (icon && (typeof icon === 'string') && this._sideIconDiv.hasClass(icon)) {
            return /* Side icon already set with given icon type */
        }

        if (!this.hasHTML()) {
            return
        }

        this.getHTML().addClass('side-icon-visible')
        this._sideIconDiv.removeClass('success warning error progress custom')

        if (typeof icon === 'string') {
            this._sideIconDiv.addClass(icon)

            if (!(this._sideIcon instanceof StockIcon)) {
                this._sideIcon = new StockIcon({name: 'success', scale: 0.75})
            }

            switch (icon) {
                case 'success':
                    this._sideIcon = this._sideIcon.alter({
                        name: 'check', variant: 'green',
                        activeName: 'check', activeVariant: 'green'
                    })

                    break

                case 'warning':
                    this._sideIcon = this._sideIcon.alter({
                        name: 'exclam', variant: 'warning',
                        activeName: 'exclam', activeVariant: 'warning-active'
                    })

                    /* Toggle warning visibility */
                    clickCallback = clickCallback || function () {
                        this.getHTML().toggleClass('warning-visible')
                    }.bind(this)

                    break

                case 'error':
                    this._sideIcon = this._sideIcon.alter({
                        name: 'exclam', variant: 'error',
                        activeName: 'exclam', activeVariant: 'error-active'
                    })

                    /* Toggle error visibility */
                    clickCallback = clickCallback || function () {
                        this.getHTML().toggleClass('error-visible')
                    }.bind(this)

                    break

                case 'progress':
                    this._sideIcon = this._sideIcon.alter({
                        name: 'sync', variant: 'green',
                        activeName: 'sync', activeVariant: 'green'
                    })

                    break

                default:
                    throw new AssertionError(`Unknown icon type ${icon}`)
            }

            this._sideIcon.applyTo(this._sideIconDiv)
        }
        else if (icon instanceof Icon) {
            this._sideIconDiv.addClass('custom')
            if (icon instanceof StockIcon) {
                icon = icon.alterDefault({scale: 0.75})
            }
            icon.applyTo(this._sideIconDiv)

            this._sideIcon = icon
        }
        else { /* Assuming null */
            this.getHTML().removeClass('side-icon-visible')
            this._sideIcon = null
        }

        this._sideIconDiv.off('click')
        if (clickCallback) {
            this._sideIconDiv.on('click', clickCallback.bind(this))
            this._sideIconDiv.css('cursor', 'pointer')
        }
        else {
            this._sideIconDiv.css('cursor', 'default')
        }
    }

    _updateSideIcon() {
        /* Use asap() here so that we can rely on the the final state when deciding what side icon to display */
        asap(function () {
            if (this.getHTML().hasClass('has-error-message')) {
                this.setSideIcon('error')
            }
            else if (this.getHTML().hasClass('has-warning-message')) {
                this.setSideIcon('warning')
            }
            else if (this.isApplied()) {
                this.setSideIcon('success')
            }
            else {
                this.setSideIcon(null)
            }
        }.bind(this))
    }


    /* Widget */

    /**
     * Return the fields's widget.
     * @returns {jQuery}
     */
    getWidget() {
        if (!this._widget) {
            this._widget = this.makeWidget()
            this.initWidget(this._widget)
        }

        return this._widget
    }

    /**
     * Create the widget's HTML element.
     *
     * Override this to implement how the widget element is created.
     *
     * @abstract
     * @returns {jQuery}
     */
    makeWidget() {
    }

    /**
     * Initialize the widget's HTML element.
     *
     * Override this to implement how the widget element is set up, after it has been created.
     *
     * @param {jQuery} widget the widget's HTML element
     */
    initWidget(widget) {
    }

    /**
     * Implement widget value validation.
     *
     * Override this to implement validation for your widget.
     *
     * @param {*} value the value to validate
     * @returns {?String} `null` if the value is valid, or an error message otherwise; if an empty string is returned,
     * the value is considered *invalid* but no error message will be shown
     */
    validateWidget(value) {
        return null
    }

    /**
     * Read value from the widget and adapt it to the field.
     *
     * Override this to implement how a value is read from the widget.
     *
     * @abstract
     * @returns {*} the field value
     */
    widgetToValue() {
    }

    /**
     * Adapt and write a value to the widget.
     *
     * Override this to implement how a value is set to the widget.
     *
     * @abstract
     * @param {*} value the field value
     */
    valueToWidget(value) {
    }

    /**
     * Update the read-only state of the widget.
     *
     * Override this to implement how the widget's readonly state is modified.
     *
     * @abstract
     * @param {Boolean} readonly
     */
    setWidgetReadonly(readonly) {
    }

    /**
     * Enable the widget.
     *
     * Override this to implement how the widget is enabled.
     *
     * @abstract
     */
    enableWidget() {
    }

    /**
     * Disable the widget.
     *
     * Override this to implement how the widget is disabled.
     *
     * @abstract
     */
    disableWidget() {
    }

    /**
     * Return the value of the forceOneLine flag.
     * @returns {Boolean}
     */
    isForceOneLine() {
        return this._forceOneLine
    }

    /**
     * Return the width percent of the value element.
     * @returns {?Number}
     */
    getValueWidth() {
        return this._valueWidth
    }

    /**
     * Set the width percent of the value element.
     * @param {?Number} width
     */
    setValueWidth(width) {
        this._valueWidth = width
        if (!this.hasHTML()) {
            return
        }

        this.getHTML().removeClass('auto-width')
        this._labelDiv.css('width', '')
        this._valueDiv.css('width', '')

        let valueWidth = this._valueWidth
        if (valueWidth == null) {
            valueWidth = this._form._valuesWidth
        }

        let useOneLine = !this._form.isCompact() || this._forceOneLine
        if (useOneLine) {
            if (valueWidth) {
                this._labelDiv.css('width', `${(100 - valueWidth)}%`)
            }
            else {
                this.getHTML().addClass('auto-width')
            }
        }
        if (valueWidth && useOneLine) {
            this._valueDiv.css('width', `${valueWidth}%`)
        }
    }

    /**
     * Return the owning form.
     * @returns {qui.forms.Form}
     */
    getForm() {
        return this._form
    }

    /**
     * Set the owning form.
     * @param {qui.forms.Form} form
     */
    setForm(form) {
        this._form = form
    }

}


export default FormField