From 108da48a0bd056cecf2b0c2ede06caff5efb4721 Mon Sep 17 00:00:00 2001 From: Kevin Ross Date: Sat, 5 Dec 2015 17:45:38 -0600 Subject: [PATCH] discretely separated all form input types into discrete es6 classes to allow for easy configuration/enforcement of markup/classes/structure --- Gruntfile.js | 24 +++-- grunt/configBridge.json | 4 +- js/src/autofill.js | 2 - js/src/baseInput.js | 107 +++++++++++++++---- js/src/baseToggle.js | 9 +- js/src/bootstrapMaterialDesign.js | 31 +++--- js/src/checkbox.js | 13 ++- js/src/{fileinput.js => file.js} | 36 +++---- js/src/radio.js | 12 +-- js/src/select.js | 87 ++++++++++++++++ js/src/switch.js | 1 - js/src/text.js | 97 +++++++++++++++++ js/src/textInput.js | 167 ------------------------------ js/src/textarea.js | 85 +++++++++++++++ js/src/util.js | 4 + scss/includes/_inputs.scss | 4 +- scss/includes/_navbar.scss | 4 +- 17 files changed, 426 insertions(+), 261 deletions(-) rename js/src/{fileinput.js => file.js} (80%) create mode 100644 js/src/select.js create mode 100644 js/src/text.js delete mode 100644 js/src/textInput.js create mode 100644 js/src/textarea.js diff --git a/Gruntfile.js b/Gruntfile.js index dc3e7e38..e709d284 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -105,11 +105,13 @@ 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/textInput.js': 'js/src/textInput.js', + 'dist/js/babel/text.js': 'js/src/text.js', + 'dist/js/babel/textarea.js': 'js/src/textarea.js', + 'dist/js/babel/select.js': 'js/src/select.js', 'dist/js/babel/checkbox.js': 'js/src/checkbox.js', 'dist/js/babel/switch.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/fileInput.js': 'js/src/file.js', 'dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', } }, @@ -128,11 +130,13 @@ 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/textInput.js': 'js/src/textInput.js', + 'docs/dist/js/babel/text.js': 'js/src/text.js', + 'docs/dist/js/babel/textarea.js': 'js/src/textarea.js', + 'docs/dist/js/babel/select.js': 'js/src/select.js', 'docs/dist/js/babel/checkbox.js': 'js/src/checkbox.js', 'docs/dist/js/babel/switch.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/fileInput.js': 'js/src/file.js', 'docs/dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', } }, @@ -154,11 +158,13 @@ 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/textInput.js': 'js/src/textInput.js', + 'dist/js/umd/text.js': 'js/src/text.js', + 'dist/js/umd/textarea.js': 'js/src/textarea.js', + 'dist/js/umd/select.js': 'js/src/select.js', 'dist/js/umd/checkbox.js': 'js/src/checkbox.js', 'dist/js/umd/switch.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/fileInput.js': 'js/src/file.js', 'dist/js/umd/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js', } } @@ -216,11 +222,13 @@ module.exports = function (grunt) { 'dist/js/babel/util.js', 'dist/js/babel/ripples.js', 'dist/js/babel/autofill.js', - 'dist/js/babel/textInput.js', + 'dist/js/babel/text.js', + 'dist/js/babel/textarea.js', + 'dist/js/babel/select.js', 'dist/js/babel/checkbox.js', 'dist/js/babel/switch.js', 'dist/js/babel/radio.js', - 'dist/js/babel/fileInput.js', + 'dist/js/babel/file.js', 'dist/js/babel/bootstrapMaterialDesign.js', ], dest: 'dist/js/<%= pkg.name %>.js' diff --git a/grunt/configBridge.json b/grunt/configBridge.json index 105e4ebf..91fe6c2a 100644 --- a/grunt/configBridge.json +++ b/grunt/configBridge.json @@ -15,7 +15,9 @@ "../dist/js/babel/bootstrapMaterialDesign.js", "../dist/js/babel/checkbox.js", "../dist/js/babel/fileInput.js", - "../dist/js/babel/textInput.js", + "../dist/js/babel/text.js", + "../dist/js/babel/textarea.js", + "../dist/js/babel/select.js", "../dist/js/babel/radio.js", "../dist/js/babel/ripples.js", "../dist/js/babel/switch.js", diff --git a/js/src/autofill.js b/js/src/autofill.js index ceb2a1f9..6dd49990 100644 --- a/js/src/autofill.js +++ b/js/src/autofill.js @@ -1,5 +1,3 @@ -//import Util from './util' - const Autofill = (($) => { /** diff --git a/js/src/baseInput.js b/js/src/baseInput.js index 6fcad9f3..529d49a9 100644 --- a/js/src/baseInput.js +++ b/js/src/baseInput.js @@ -1,13 +1,16 @@ +import Util from './util' + const BaseInput = (($) => { const Default = { formGroup: { template: `
`, required: true, - autoCreate: false + autoCreate: true }, - requiredClasses: ['form-control'], - invalidComponentMatches: [] + requiredClasses: [], + invalidComponentMatches: [], + convertInputSizeVariations: true } const ClassName = { @@ -20,6 +23,11 @@ const BaseInput = (($) => { FORM_GROUP: `.${ClassName.FORM_GROUP}` //, } + const FormControlSizeConversions = { + 'form-control-lg': 'form-group-lg', + 'form-control-sm': 'form-group-sm' + } + /** * ------------------------------------------------------------------------ * Class Definition @@ -27,9 +35,9 @@ const BaseInput = (($) => { */ class BaseInput { - constructor(element, defaultConfig, config) { + constructor(element, config) { this.$element = $(element) - this.config = $.extend({}, Default, defaultConfig, config) + this.config = $.extend({}, Default, config) // Enforce no overlap between components to prevent side effects this._rejectInvalidComponentMatches() @@ -48,6 +56,8 @@ const BaseInput = (($) => { // different components have different rules, always run this separately this.$formGroup = this.findFormGroup(this.config.formGroup.required) + this._convertFormControlSizeVariations() + this.addFocusListener() this.addChangeListener() } @@ -67,11 +77,45 @@ const BaseInput = (($) => { } addFocusListener() { - // implement + this.$element + .on('focus', () => { + this.addFormGroupFocus() + }) + .on('blur', () => { + this.removeFormGroupFocus() + }) } addChangeListener() { - // implement + this.$element + .on('keydown paste', (event) => { + if (Util.isChar(event)) { + this.removeIsEmpty() + } + }) + .on('keyup change', (event) => { + + // 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.$element.val()) { + this.addIsEmpty() + } else { + this.removeIsEmpty() + } + + // 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.removeHasError() + } else { + this.addHasError() + } + }) } addFormGroupFocus() { @@ -120,16 +164,7 @@ const BaseInput = (($) => { 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 ${this.$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) + $.error(`Failed to find form-group for ${Util.describe(this.$element)}`) } return fg } @@ -137,15 +172,45 @@ const BaseInput = (($) => { // ------------------------------------------------------------------------ // private _rejectInvalidComponentMatches() { - for (let otherComponent in this.config.invalidComponentMatches) { + for (let otherComponent of this.config.invalidComponentMatches) { otherComponent.rejectMatch(this.constructor.name, this.$element) } } _rejectWithoutRequiredClasses() { - for (let requiredClass in this.config.requiredClasses) { - if (!this.$element.hasClass(requiredClass)) { - $.error(`${this.constructor.name} elements require class: ${requiredClass}`) + 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}`) + } + } + } + + _convertFormControlSizeVariations() { + 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 FormControlSizeConversions) { + if (this.$element.hasClass(inputSize)) { + this.$element.removeClass(inputSize) + this.$formGroup.addClass(FormControlSizeConversions[inputSize]) } } } diff --git a/js/src/baseToggle.js b/js/src/baseToggle.js index 1ed10d51..9c8cf93c 100644 --- a/js/src/baseToggle.js +++ b/js/src/baseToggle.js @@ -1,8 +1,4 @@ import BaseInput from './baseInput' -//import TextInput from './textInput' -//import FileInput from './fileInput' -//import Radio from './radio' -//import Switch from './switch' import Util from './util' const BaseToggle = (($) => { @@ -13,9 +9,6 @@ const BaseToggle = (($) => { * ------------------------------------------------------------------------ */ const Default = { - formGroup: { - autoCreate: true - } } const Selector = { @@ -30,7 +23,7 @@ const BaseToggle = (($) => { class BaseToggle extends BaseInput { constructor(element, config, inputType, outerClass) { - super(element, Default, config) + super(element, $.extend({}, Default, config)) this.$element.after(this.config.template) // '.checkbox|switch|radio > label > input[type=checkbox|radio]' // '.${this.outerClass} > label > input[type=${this.inputType}]' diff --git a/js/src/bootstrapMaterialDesign.js b/js/src/bootstrapMaterialDesign.js index 7de86ea8..39fe0d22 100644 --- a/js/src/bootstrapMaterialDesign.js +++ b/js/src/bootstrapMaterialDesign.js @@ -1,15 +1,8 @@ -//import Util from './util' - /** * $.bootstrapMaterialDesign(config) is a macro class to configure the components generally * used in Material Design for Bootstrap. You may pass overrides to the configurations * which will be passed into each component, or you may omit use of this class and * configure each component separately. - * - * 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, - * switch, fileInput. - * */ const BootstrapMaterialDesign = (($) => { @@ -42,12 +35,14 @@ const BootstrapMaterialDesign = (($) => { '.ripple' // generic marker class to add ripple to elements ] }, - textInput: { - selector: [ - 'input[type=text]', - 'textarea', - 'select' - ] + text: { + selector: ['input[type=text]'] + }, + textarea: { + selector: ['textarea'] + }, + select: { + selector: ['select'] }, checkbox: { selector: '.checkbox > label > input[type=checkbox]' @@ -58,7 +53,7 @@ const BootstrapMaterialDesign = (($) => { radio: { selector: '.radio > label > input[type=radio]' }, - fileInput: { + file: { selector: 'input[type=file]' }, autofill: { @@ -68,11 +63,13 @@ const BootstrapMaterialDesign = (($) => { // create an ordered component list for instantiation instantiation: [ 'ripples', - 'textInput', 'checkbox', - 'switch', + 'file', 'radio', - 'fileInput', + 'switch', + 'text', + 'textarea', + 'select', 'autofill' ] } diff --git a/js/src/checkbox.js b/js/src/checkbox.js index 1df78aea..f040bc9b 100644 --- a/js/src/checkbox.js +++ b/js/src/checkbox.js @@ -1,7 +1,9 @@ import BaseToggle from './baseToggle' -import TextInput from './textInput' -import FileInput from './fileInput' +import Text from './text' +import File from './file' import Radio from './radio' +import Textarea from './textare' +import Select from './select' import Util from './util' const Checkbox = (($) => { @@ -16,8 +18,7 @@ const Checkbox = (($) => { const JQUERY_NO_CONFLICT = $.fn[NAME] const Default = { - template: ``, - invalidComponentMatches: [FileInput, Radio, TextInput] + template: `` } /** @@ -28,7 +29,9 @@ const Checkbox = (($) => { class Checkbox extends BaseToggle { constructor(element, config, inputType = NAME, outerClass = NAME) { - super(element, $.extend({}, Default, config), inputType, outerClass) + super(element, $.extend({ + invalidComponentMatches: [File, Radio, Text, Textarea, Select] + }, Default, config), inputType, outerClass) } dispose() { diff --git a/js/src/fileinput.js b/js/src/file.js similarity index 80% rename from js/src/fileinput.js rename to js/src/file.js index 428672de..7a09c886 100644 --- a/js/src/fileinput.js +++ b/js/src/file.js @@ -2,30 +2,24 @@ import BaseInput from './baseInput' import Checkbox from './checkbox' import Radio from './radio' import Switch from './switch' -import TextInput from './textInput' +import Text from './text' +import Textarea from './textare' +import Select from './select' import Util from './util' -// FileInput decorator, to be called after Input -const FileInput = (($) => { +const File = (($) => { /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ - const NAME = 'fileInput' + const NAME = 'file' const DATA_KEY = `mdb.${NAME}` const JQUERY_NO_CONFLICT = $.fn[NAME] - const Default = { - formGroup: { - autoCreate: true - }, - invalidComponentMatches: [Checkbox, Radio, Switch, TextInput] - } - const ClassName = { - IS_FILEINPUT: 'is-fileinput' + IS_FILE: 'is-file' } const Selector = { @@ -37,12 +31,12 @@ const FileInput = (($) => { * Class Definition * ------------------------------------------------------------------------ */ - class FileInput extends BaseInput { + class File extends BaseInput { constructor(element, config) { - super(element, Default, config) + super(element, $.extend({invalidComponentMatches: [Checkbox, Radio, Text, Textarea, Select, Switch]}, config)) - this.$formGroup.addClass(ClassName.IS_FILEINPUT) + this.$formGroup.addClass(ClassName.IS_FILE) } dispose() { @@ -105,7 +99,7 @@ const FileInput = (($) => { let data = $element.data(DATA_KEY) if (!data) { - data = new FileInput(this, config) + data = new File(this, config) $element.data(DATA_KEY, data) } }) @@ -117,15 +111,15 @@ const FileInput = (($) => { * jQuery * ------------------------------------------------------------------------ */ - $.fn[NAME] = FileInput._jQueryInterface - $.fn[NAME].Constructor = FileInput + $.fn[NAME] = File._jQueryInterface + $.fn[NAME].Constructor = File $.fn[NAME].noConflict = () => { $.fn[NAME] = JQUERY_NO_CONFLICT - return FileInput._jQueryInterface + return File._jQueryInterface } - return FileInput + return File })(jQuery) -export default FileInput +export default File diff --git a/js/src/radio.js b/js/src/radio.js index d32cef7a..20dc51a4 100644 --- a/js/src/radio.js +++ b/js/src/radio.js @@ -1,11 +1,10 @@ import BaseToggle from './baseToggle' -import TextInput from './textInput' -import FileInput from './fileInput' +import Text from './text' +import File from './file' import Checkbox from './checkbox' import Switch from './switch' import Util from './util' -// Radio decorator, to be called after Input const Radio = (($) => { /** @@ -18,8 +17,7 @@ const Radio = (($) => { const JQUERY_NO_CONFLICT = $.fn[NAME] const Default = { - template: ``, - invalidComponentMatches: [Checkbox, FileInput, Switch, TextInput] + template: `` } /** @@ -30,7 +28,9 @@ const Radio = (($) => { class Radio extends BaseToggle { constructor(element, config) { - super(element, $.extend({}, Default, config), NAME, NAME) + super(element, $.extend({ + invalidComponentMatches: [Checkbox, File, Switch, Text] + }, Default, config), NAME, NAME) } dispose() { diff --git a/js/src/select.js b/js/src/select.js new file mode 100644 index 00000000..dfd7e8ae --- /dev/null +++ b/js/src/select.js @@ -0,0 +1,87 @@ +import Checkbox from './checkbox' +import File from './file' +import Radio from './radio' +import Switch from './switch' +import Text from './text' +import Textarea from './textare' +import Util from './util' + +const Select = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'select' + const DATA_KEY = `mdb.${NAME}` + const JQUERY_NO_CONFLICT = $.fn[NAME] + + const Default = { + requiredClasses: ['form-control||c-select'] + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Select extends Text { + + constructor(element, config) { + super(element, $.extend({invalidComponentMatches: [Checkbox, File, Radio, Switch, Text, Textarea]}, Default, config)) + } + + dispose() { + super.dispose(DATA_KEY) + } + + static matches($element) { + if ($element.prop('tagName') === 'select') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.matches($element), `${component} component is invalid for