1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
|
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',
INPUT_GROUP: 'input-group'
}
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
},
bmdFormGroup: {
template: `<span class='${ClassName.BMD_FORM_GROUP}'></span>`,
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 $bmdFormGroup.find(selector)
//
// Note this only runs if $bmdFormGroup.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.$bmdFormGroup = this.resolveMdbFormGroup()
// Resolve and mark the bmdLabel if necessary as defined by the config
this.$bmdLabel = this.resolveMdbLabel()
// Signal to the bmd-form-group that a form-control-* variation is being used
this.resolveMdbFormGroupSizing()
this.addFocusListener()
this.addChangeListener()
if(this.$element.val() != ''){
this.addIsFilled()
}
}
dispose(dataKey) {
super.dispose(dataKey)
this.$bmdFormGroup = 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.$bmdFormGroup.addClass(ClassName.HAS_DANGER)
}
removeHasDanger() {
this.$bmdFormGroup.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.bmdFormGroup.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 may be an input-group, wrap that instead
if(this.outerElement().parent().hasClass(ClassName.INPUT_GROUP)){
this.outerElement().parent().wrap(this.config.bmdFormGroup.template)
}
else{
this.outerElement().wrap(this.config.bmdFormGroup.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.bmdFormGroup.template)
}
mfg = this.findMdbFormGroup(this.config.bmdFormGroup.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.$bmdFormGroup.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.$bmdFormGroup.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.$bmdFormGroup.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
|