import Base from './base' import Util from './util' const BaseInput = (($) => { const ClassName = { FORM_GROUP: 'form-group', BMD_FORM_GROUP: 'bmd-form-group', BMD_LABEL: 'bmd-label', BMD_LABEL_STATIC: 'bmd-label-static', BMD_LABEL_PLACEHOLDER: 'bmd-label-placeholder', BMD_LABEL_FLOATING: 'bmd-label-floating', HAS_DANGER: 'has-danger', IS_FILLED: 'is-filled', IS_FOCUSED: 'is-focused' } const Selector = { FORM_GROUP: `.${ClassName.FORM_GROUP}`, BMD_FORM_GROUP: `.${ClassName.BMD_FORM_GROUP}`, BMD_LABEL_WILDCARD: `label[class^='${ClassName.BMD_LABEL}'], label[class*=' ${ClassName.BMD_LABEL}']` // match any label variant if specified } const Default = { validate: false, formGroup: { required: false }, mdbFormGroup: { template: ``, create: true, // create a wrapper if form-group not found required: true // not recommended to turn this off, only used for inline components }, label: { required: false, // Prioritized find order for resolving the label to be used as an bmd-label if not specified in the markup // - a function(thisComponent); or // - a string selector used like $mdbFormGroup.find(selector) // // Note this only runs if $mdbFormGroup.find(Selector.BMD_LABEL_WILDCARD) fails to find a label (as authored in the markup) // selectors: [ `.form-control-label`, // in the case of horizontal or inline forms, this will be marked `> label` // usual case for text inputs, first child. Deeper would find toggle labels so don't do that. ], className: ClassName.BMD_LABEL_STATIC }, requiredClasses: [], invalidComponentMatches: [], convertInputSizeVariations: true } const FormControlSizeMarkers = { 'form-control-lg': 'bmd-form-group-lg', 'form-control-sm': 'bmd-form-group-sm' } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ class BaseInput extends Base { /** * * @param element * @param config * @param properties - anything that needs to be set as this[key] = value. Works around the need to call `super` before using `this` */ constructor($element, config, properties = {}) { super($element, $.extend(true, {}, Default, config), properties) // Enforce no overlap between components to prevent side effects this._rejectInvalidComponentMatches() // Enforce expected structure (if any) this.rejectWithoutRequiredStructure() // Enforce required classes for a consistent rendering this._rejectWithoutRequiredClasses() // Resolve the form-group first, it will be used for bmd-form-group if possible // note: different components have different rules this.$formGroup = this.findFormGroup(this.config.formGroup.required) // Will add bmd-form-group to form-group or create an bmd-form-group // Performance Note: for those forms that are really performance driven, create the markup with the .bmd-form-group to avoid // rendering changes once added. this.$mdbFormGroup = this.resolveMdbFormGroup() // Resolve and mark the mdbLabel if necessary as defined by the config this.$mdbLabel = this.resolveMdbLabel() // Signal to the bmd-form-group that a form-control-* variation is being used this.resolveMdbFormGroupSizing() this.addFocusListener() this.addChangeListener() } dispose(dataKey) { super.dispose(dataKey) this.$mdbFormGroup = null this.$formGroup = null } // ------------------------------------------------------------------------ // protected rejectWithoutRequiredStructure() { // implement } addFocusListener() { this.$element .on('focus', () => { this.addFormGroupFocus() }) .on('blur', () => { this.removeFormGroupFocus() }) } addChangeListener() { this.$element .on('keydown paste', (event) => { if (Util.isChar(event)) { this.addIsFilled() } }) .on('keyup change', () => { // make sure empty is added back when there is a programmatic value change. // NOTE: programmatic changing of value using $.val() must trigger the change event i.e. $.val('x').trigger('change') if (this.isEmpty()) { this.removeIsFilled() } else { this.addIsFilled() } if (this.config.validate) { // Validation events do not bubble, so they must be attached directly to the text: http://jsfiddle.net/PEpRM/1/ // Further, even the bind method is being caught, but since we are already calling #checkValidity here, just alter // the form-group on change. // // NOTE: I'm not sure we should be intervening regarding validation, this seems better as a README and snippet of code. // BUT, I've left it here for backwards compatibility. let isValid = (typeof this.$element[0].checkValidity === 'undefined' || this.$element[0].checkValidity()) if (isValid) { this.removeHasDanger() } else { this.addHasDanger() } } }) } addHasDanger() { this.$mdbFormGroup.addClass(ClassName.HAS_DANGER) } removeHasDanger() { this.$mdbFormGroup.removeClass(ClassName.HAS_DANGER) } isEmpty() { return (this.$element.val() === null || this.$element.val() === undefined || this.$element.val() === '') } // Will add bmd-form-group to form-group or create a bmd-form-group if necessary resolveMdbFormGroup() { let mfg = this.findMdbFormGroup(false) if (mfg === undefined || mfg.length === 0) { if (this.config.mdbFormGroup.create && (this.$formGroup === undefined || this.$formGroup.length === 0)) { // If a form-group doesn't exist (not recommended), take a guess and wrap the element (assuming no label). // note: it's possible to make this smarter, but I need to see valid cases before adding any complexity. this.outerElement().wrap(this.config.mdbFormGroup.template) } else { // a form-group does exist, add our marker class to it this.$formGroup.addClass(ClassName.BMD_FORM_GROUP) // OLD: may want to implement this after all, see how the styling turns out, but using an existing form-group is less manipulation of the dom and therefore preferable // A form-group does exist, so add an bmd-form-group wrapping it's internal contents //fg.wrapInner(this.config.mdbFormGroup.template) } mfg = this.findMdbFormGroup(this.config.mdbFormGroup.required) } return mfg } // Demarcation element (e.g. first child of a form-group) // Subclasses such as file inputs may have different structures outerElement() { return this.$element } // Will add bmd-label to bmd-form-group if not already specified resolveMdbLabel() { let label = this.$mdbFormGroup.find(Selector.BMD_LABEL_WILDCARD) if (label === undefined || label.length === 0) { // we need to find it based on the configured selectors label = this.findMdbLabel(this.config.label.required) if (label === undefined || label.length === 0) { // no label found, and finder did not require one } else { // a candidate label was found, add the configured default class name label.addClass(this.config.label.className) } } return label } // Find bmd-label variant based on the config selectors findMdbLabel(raiseError = true) { let label = null // use the specified selector order for (let selector of this.config.label.selectors) { if ($.isFunction(selector)) { label = selector(this) } else { label = this.$mdbFormGroup.find(selector) } if (label !== undefined && label.length > 0) { break } } if (label.length === 0 && raiseError) { $.error(`Failed to find ${Selector.BMD_LABEL_WILDCARD} within form-group for ${Util.describe(this.$element)}`) } return label } // Find bmd-form-group findFormGroup(raiseError = true) { let fg = this.$element.closest(Selector.FORM_GROUP) if (fg.length === 0 && raiseError) { $.error(`Failed to find ${Selector.FORM_GROUP} for ${Util.describe(this.$element)}`) } return fg } // Due to the interconnected nature of labels/inputs/help-blocks, signal the bmd-form-group-* size variation based on // a found form-control-* size resolveMdbFormGroupSizing() { if (!this.config.convertInputSizeVariations) { return } // Modification - Change text-sm/lg to form-group-sm/lg instead (preferred standard and simpler css/less variants) for (let inputSize in FormControlSizeMarkers) { if (this.$element.hasClass(inputSize)) { //this.$element.removeClass(inputSize) this.$mdbFormGroup.addClass(FormControlSizeMarkers[inputSize]) } } } // ------------------------------------------------------------------------ // private _rejectInvalidComponentMatches() { for (let otherComponent of this.config.invalidComponentMatches) { otherComponent.rejectMatch(this.constructor.name, this.$element) } } _rejectWithoutRequiredClasses() { for (let requiredClass of this.config.requiredClasses) { let found = false // allow one of several classes to be passed in x||y if (requiredClass.indexOf('||') !== -1) { let oneOf = requiredClass.split('||') for (let requiredClass of oneOf) { if (this.$element.hasClass(requiredClass)) { found = true break } } } else if (this.$element.hasClass(requiredClass)) { found = true } // error if not found if (!found) { $.error(`${this.constructor.name} element: ${Util.describe(this.$element)} requires class: ${requiredClass}`) } } } // ------------------------------------------------------------------------ // static } return BaseInput })(jQuery) export default BaseInput