diff --git a/Gruntfile.js b/Gruntfile.js index 87d78e9e..70def36a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -103,9 +103,9 @@ module.exports = function (grunt) { 'dist/js/babel/util.js': 'js/src/util.js', 'dist/js/babel/ripples.js': 'js/src/ripples.js', 'dist/js/babel/autofill.js': 'js/src/autofill.js', - 'dist/js/babel/input.js': 'js/src/input.js', + 'dist/js/babel/input.js': 'js/src/textInput.js', 'dist/js/babel/checkbox.js': 'js/src/checkbox.js', - 'dist/js/babel/togglebutton.js': 'js/src/togglebutton.js', + 'dist/js/babel/togglebutton.js': 'js/src/switch.js', 'dist/js/babel/radio.js': 'js/src/radio.js', 'dist/js/babel/fileInput.js': 'js/src/fileInput.js', 'dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', @@ -124,9 +124,9 @@ module.exports = function (grunt) { 'docs/dist/js/babel/util.js': 'js/src/util.js', 'docs/dist/js/babel/ripples.js': 'js/src/ripples.js', 'docs/dist/js/babel/autofill.js': 'js/src/autofill.js', - 'docs/dist/js/babel/input.js': 'js/src/input.js', + 'docs/dist/js/babel/input.js': 'js/src/textInput.js', 'docs/dist/js/babel/checkbox.js': 'js/src/checkbox.js', - 'docs/dist/js/babel/togglebutton.js': 'js/src/togglebutton.js', + 'docs/dist/js/babel/togglebutton.js': 'js/src/switch.js', 'docs/dist/js/babel/radio.js': 'js/src/radio.js', 'docs/dist/js/babel/fileInput.js': 'js/src/fileInput.js', 'docs/dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', @@ -148,9 +148,9 @@ module.exports = function (grunt) { 'dist/js/umd/util.js': 'js/src/util.js', 'dist/js/umd/ripples.js': 'js/src/ripples.js', 'dist/js/umd/autofill.js': 'js/src/autofill.js', - 'dist/js/umd/input.js': 'js/src/input.js', + 'dist/js/umd/input.js': 'js/src/textInput.js', 'dist/js/umd/checkbox.js': 'js/src/checkbox.js', - 'dist/js/umd/togglebutton.js': 'js/src/togglebutton.js', + 'dist/js/umd/togglebutton.js': 'js/src/switch.js', 'dist/js/umd/radio.js': 'js/src/radio.js', 'dist/js/umd/fileInput.js': 'js/src/fileInput.js', 'dist/js/umd/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', @@ -208,9 +208,9 @@ module.exports = function (grunt) { 'dist/js/babel/util.js', 'dist/js/babel/ripples.js', 'dist/js/babel/autofill.js', - 'dist/js/babel/input.js', + 'dist/js/babel/textInput.js', 'dist/js/babel/checkbox.js', - 'dist/js/babel/togglebutton.js', + 'dist/js/babel/switch.js', 'dist/js/babel/radio.js', 'dist/js/babel/fileInput.js', 'dist/js/babel/bootstrapMaterialDesign.js', diff --git a/grunt/configBridge.json b/grunt/configBridge.json index b63fd5ac..2f0bdd94 100644 --- a/grunt/configBridge.json +++ b/grunt/configBridge.json @@ -9,6 +9,7 @@ ], "coreJs": [ + "../dist/js/babel/baseInput.js", "../dist/js/babel/autofill.js", "../dist/js/babel/bootstrapMaterialDesign.js", "../dist/js/babel/checkbox.js", diff --git a/js/src/baseInput.js b/js/src/baseInput.js new file mode 100644 index 00000000..c8d61294 --- /dev/null +++ b/js/src/baseInput.js @@ -0,0 +1,164 @@ +import Util from './util' + +const BaseInput = (($) => { + + const Default = { + formGroup: { + template: `
`, + required: true, + autoCreate: false + }, + requiredClasses: ['form-control'], + invalidComponentMatches: [] + } + + const ClassName = { + FORM_GROUP: 'form-group', + HAS_ERROR: 'has-error', + IS_EMPTY: 'is-empty' + } + + const Selector = { + FORM_GROUP: `.${ClassName.FORM_GROUP}` //, + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class BaseInput { + + constructor(element, defaultConfig, config) { + this.$element = $(element) + this.config = $.extend({}, Default, defaultConfig, config) + + // Enforce no overlap between components to prevent side effects + this._rejectInvalidComponentMatches() + + // Enforce required classes for a consistent rendering + this._rejectWithoutRequiredClasses() + + // Enforce expected structure (if any) + this.rejectWithoutRequiredStructure() + + if(this.config.formGroup.autoCreate) { + // Will create form-group if necessary + this.autoCreateFormGroup() + } + + // different components have different rules, always run this separately + this.$formGroup = this.findFormGroup(this.config.formGroup.required) + + this.addFocusListener() + this.addChangeListener() + } + + dispose(dataKey) { + $.removeData(this.$element, dataKey) + this.$element = null + this.$formGroup = null + this.config = null + } + + // ------------------------------------------------------------------------ + // protected + + rejectWithoutRequiredStructure(){ + // implement + } + + addFocusListener() { + // implement + } + + addChangeListener() { + // implement + } + + addFormGroupFocus(formGroup) { + formGroup.addClass(ClassName.IS_FOCUSED) + } + + removeFormGroupFocus(formGroup) { + formGroup.removeClass(ClassName.IS_FOCUSED) + } + + addHasError() { + this.$formGroup.addClass(ClassName.HAS_ERROR) + } + + removeHasError() { + this.$formGroup.removeClass(ClassName.HAS_ERROR) + } + + addIsEmpty() { + this.$formGroup.addClass(ClassName.IS_EMPTY) + } + + removeIsEmpty() { + this.$formGroup.removeClass(ClassName.IS_EMPTY) + } + + isEmpty() { + return (this.$element.val() === null || this.$element.val() === undefined || this.$element.val() === '') + } + + // Find or create a form-group if necessary + autoCreateFormGroup() { + let fg = this.findFormGroup(false) + if (fg === null || fg.length === 0) { + this.outerElement().wrap(this.config.formGroup.template) + } + } + + // Demarcation element (e.g. first child of a form-group) + // Subclasses such as file inputs may have different structures + outerElement(){ + return this.$element + } + + // Find expected form-group + findFormGroup(raiseError = true) { + let fg = this.$element.closest(Selector.FORM_GROUP) // note that form-group may be grandparent in the case of an input-group + if (fg.length === 0 && raiseError) { + $.error(`Failed to find form-group for ${$element}`) + } + return fg + } + + findOrCreateFormGroup() { + let fg = this.$element.closest(Selector.FORM_GROUP) // note that form-group may be grandparent in the case of an baseInput-group + if (fg === null || fg.length === 0) { + this.$element.wrap(this.config.formGroup.template) + fg = this.$element.closest(Selector.FORM_GROUP) // find node after attached (otherwise additional attachments don't work) + } + return fg + } + + // ------------------------------------------------------------------------ + // private + _rejectInvalidComponentMatches(){ + for(let otherComponent in this.config.invalidComponentMatches){ + otherComponent.rejectMatch(this.constructor.name, $element) + } + } + + _rejectWithoutRequiredClasses(){ + for(let requiredClass in this.config.requiredClasses){ + if(!$element.hasClass(requiredClass)){ + $.error(`${this.constructor.name} elements require class: ${requiredClass}`) + } + } + } + + // ------------------------------------------------------------------------ + // static + + } + + return BaseInput + +})(jQuery) + +export default BaseInput diff --git a/js/src/bootstrapMaterialDesign.js b/js/src/bootstrapMaterialDesign.js index 21925d6c..231c5ecc 100644 --- a/js/src/bootstrapMaterialDesign.js +++ b/js/src/bootstrapMaterialDesign.js @@ -8,7 +8,7 @@ * * NOTE: If omitting use of this class, please note that the Input component must be * initialized prior to other decorating components such as radio, checkbox, - * togglebutton, fileInput. + * switch, fileInput. * */ const BootstrapMaterialDesign = (($) => { @@ -42,18 +42,18 @@ const BootstrapMaterialDesign = (($) => { '.ripple' // generic marker class to add ripple to elements ] }, - input: { + textInput: { selector: [ - 'input.form-control', - 'textarea.form-control', - 'select.form-control' + 'input[type=text]', + 'textarea', + 'select' ] }, checkbox: { selector: '.checkbox > label > input[type=checkbox]' }, - togglebutton: { - selector: '.togglebutton > label > input[type=checkbox]' + switch: { + selector: '.switch > label > input[type=checkbox]' }, radio: { selector: '.radio > label > input[type=radio]' @@ -68,9 +68,9 @@ const BootstrapMaterialDesign = (($) => { // create an ordered component list for instantiation instantiation: [ 'ripples', - 'input', + 'textInput', 'checkbox', - 'togglebutton', + 'switch', 'radio', 'fileInput', 'autofill' diff --git a/js/src/checkbox.js b/js/src/checkbox.js index 647a06e0..583c3156 100644 --- a/js/src/checkbox.js +++ b/js/src/checkbox.js @@ -1,6 +1,10 @@ +import BaseInput from './baseInput' +import TextInput from './textInput' +import FileInput from './fileInput' +import Radio from './radio' +import Switch from './switch' import Util from './util' -// Checkbox decorator, to be called after Input const Checkbox = (($) => { /** @@ -13,7 +17,11 @@ const Checkbox = (($) => { const JQUERY_NO_CONFLICT = $.fn[NAME] const Default = { - template: `` + template: ``, + formGroup: { + autoCreate: true + }, + invalidComponentMatches: [TextInput, FileInput, Radio, Switch] } /** @@ -21,40 +29,66 @@ const Checkbox = (($) => { * Class Definition * ------------------------------------------------------------------------ */ - class Checkbox { + class Checkbox extends BaseInput { constructor(element, config) { - this.$element = $(element) - this.config = $.extend({}, Default, config) - + super(element, Default, config) this.$element.after(this.config.template) - this.$formGroup = Util.findFormGroup(this.$element, false) - - this._bindEventListeners() } dispose() { - $.removeData(this.$element, DATA_KEY) - this.$element = null - this.$formGroup = null - this.config = null + super.dispose(DATA_KEY) + } + + static matches($element) { + // '.checkbox > label > input[type=checkbox]' + if ($element.attr('type') === 'checkbox') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.matches($element), `${component} component is invalid for type='checkbox'.`) } // ------------------------------------------------------------------------ - // private - _bindEventListeners() { + // protected + + // Demarcation element (e.g. first child of a form-group) + // Subclasses such as file inputs may have different structures + outerElement() { + // '.checkbox > label > input[type=checkbox]' + return this.$element.parent().parent() + } + + rejectWithoutRequiredStructure() { + // '.checkbox > label > input[type=checkbox]' + Util.assert(this.$element.parent().prop('tagName') === 'label', `${component} parent element should be