diff options
Diffstat (limited to 'templates/assets/bootstrap-material-design/js')
21 files changed, 2543 insertions, 0 deletions
diff --git a/templates/assets/bootstrap-material-design/js/autofill.js b/templates/assets/bootstrap-material-design/js/autofill.js new file mode 100644 index 0000000..2ada9ec --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/autofill.js @@ -0,0 +1,109 @@ +import Base from './base' + +const Autofill = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'autofill' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = {} + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Autofill extends Base { + + constructor($element, config) { + super($element, $.extend(true, {}, Default, config)) + + this._watchLoading() + this._attachEventHandlers() + } + + dispose() { + super.dispose(DATA_KEY) + } + + // ------------------------------------------------------------------------ + // private + + _watchLoading() { + // After 10 seconds we are quite sure all the needed inputs are autofilled then we can stop checking them + setTimeout(() => { + clearInterval(this._onLoading) + }, 10000) + } + + // This part of code will detect autofill when the page is loading (username and password inputs for example) + _onLoading() { + setInterval(() => { + $('input[type!=checkbox]').each((index, element) => { + let $element = $(element) + if ($element.val() && $element.val() !== $element.attr('value')) { + $element.trigger('change') + } + }) + }, 100) + } + + _attachEventHandlers() { + // Listen on inputs of the focused form + // (because user can select from the autofill dropdown only when the input has focus) + let focused = null + $(document) + .on('focus', 'input', (event) => { + let $inputs = $(event.currentTarget).closest('form').find('input').not('[type=file]') + focused = setInterval(() => { + $inputs.each((index, element) => { + let $element = $(element) + if ($element.val() !== $element.attr('value')) { + $element.trigger('change') + } + }) + }, 100) + }) + .on('blur', '.form-group input', () => { + clearInterval(focused) + }) + } + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Autofill($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Autofill._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Autofill + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Autofill._jQueryInterface + } + + return Autofill + +})(jQuery) + +export default Autofill diff --git a/templates/assets/bootstrap-material-design/js/base.js b/templates/assets/bootstrap-material-design/js/base.js new file mode 100644 index 0000000..283b976 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/base.js @@ -0,0 +1,88 @@ +import Util from './util' + +const Base = (($) => { + + const ClassName = { + BMD_FORM_GROUP: 'bmd-form-group', + IS_FILLED: 'is-filled', + IS_FOCUSED: 'is-focused' + } + + const Selector = { + BMD_FORM_GROUP: `.${ClassName.BMD_FORM_GROUP}` + } + + const Default = {} + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class 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 = {}) { + this.$element = $element + this.config = $.extend(true, {}, Default, config) + + // set properties for use in the constructor initialization + for (let key in properties) { + this[key] = properties[key] + } + } + + dispose(dataKey) { + this.$element.data(dataKey, null) + this.$element = null + this.config = null + } + + // ------------------------------------------------------------------------ + // protected + + addFormGroupFocus() { + if (!this.$element.prop('disabled')) { + this.$bmdFormGroup.addClass(ClassName.IS_FOCUSED) + } + } + + removeFormGroupFocus() { + this.$bmdFormGroup.removeClass(ClassName.IS_FOCUSED) + } + + removeIsFilled() { + this.$bmdFormGroup.removeClass(ClassName.IS_FILLED) + } + + addIsFilled() { + this.$bmdFormGroup.addClass(ClassName.IS_FILLED) + } + + // Find bmd-form-group + findMdbFormGroup(raiseError = true) { + let mfg = this.$element.closest(Selector.BMD_FORM_GROUP) + if (mfg.length === 0 && raiseError) { + $.error(`Failed to find ${Selector.BMD_FORM_GROUP} for ${Util.describe(this.$element)}`) + } + return mfg + } + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + + } + + return Base + +})(jQuery) + +export default Base diff --git a/templates/assets/bootstrap-material-design/js/baseFormControl.js b/templates/assets/bootstrap-material-design/js/baseFormControl.js new file mode 100644 index 0000000..8481070 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/baseFormControl.js @@ -0,0 +1,36 @@ +import BaseInput from './baseInput' + +const BaseFormControl = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const Default = { + requiredClasses: ['form-control'] + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class BaseFormControl extends BaseInput { + + constructor($element, config) { + super($element, $.extend(true, Default, config)) + + // Initially mark as empty + if (this.isEmpty()) { + this.removeIsFilled() + } + } + } + + + return BaseFormControl + +})(jQuery) + +export default BaseFormControl diff --git a/templates/assets/bootstrap-material-design/js/baseInput.js b/templates/assets/bootstrap-material-design/js/baseInput.js new file mode 100644 index 0000000..5396ccb --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/baseInput.js @@ -0,0 +1,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 diff --git a/templates/assets/bootstrap-material-design/js/baseLayout.js b/templates/assets/bootstrap-material-design/js/baseLayout.js new file mode 100644 index 0000000..8dc87ed --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/baseLayout.js @@ -0,0 +1,122 @@ +import Base from './base' +import Util from './util' + +const BaseLayout = (($) => { + + const ClassName = { + CANVAS: 'bmd-layout-canvas', + CONTAINER: 'bmd-layout-container', + BACKDROP: `bmd-layout-backdrop` + } + + const Selector = { + CANVAS: `.${ClassName.CANVAS}`, + CONTAINER: `.${ClassName.CONTAINER}`, + BACKDROP: `.${ClassName.BACKDROP}` + } + + const Default = { + canvas: { + create: true, + required: true, + template: `<div class="${ClassName.CANVAS}"></div>` + }, + backdrop: { + create: true, + required: true, + template: `<div class="${ClassName.BACKDROP}"></div>` + } + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class BaseLayout extends Base { + + constructor($element, config, properties = {}) { + super($element, $.extend(true, {}, Default, config), properties) + + this.$container = this.findContainer(true) + this.$backdrop = this.resolveBackdrop() + this.resolveCanvas(); + } + + dispose(dataKey) { + super.dispose(dataKey) + this.$container = null + this.$backdrop = null + } + + // ------------------------------------------------------------------------ + // protected + + // Will wrap container in bmd-layout-canvas if necessary + resolveCanvas() { + let bd = this.findCanvas(false) + if (bd === undefined || bd.length === 0) { + if (this.config.canvas.create) { + this.$container.wrap(this.config.canvas.template) + } + + bd = this.findCanvas(this.config.canvas.required) + } + + return bd + } + + // Find closest bmd-layout-container based on the given context + findCanvas(raiseError = true, context = this.$container) { + let canvas = context.closest(Selector.CANVAS) + if (canvas.length === 0 && raiseError) { + $.error(`Failed to find ${Selector.CANVAS} for ${Util.describe(context)}`) + } + return canvas + } + + // Will add bmd-layout-backdrop to bmd-layout-container if necessary + resolveBackdrop() { + let bd = this.findBackdrop(false) + if (bd === undefined || bd.length === 0) { + if (this.config.backdrop.create) { + this.$container.append(this.config.backdrop.template) + } + + bd = this.findBackdrop(this.config.backdrop.required) + } + + return bd + } + + // Find closest bmd-layout-container based on the given context + findBackdrop(raiseError = true, context = this.$container) { + let backdrop = context.find(`> ${Selector.BACKDROP}`) + if (backdrop.length === 0 && raiseError) { + $.error(`Failed to find ${Selector.BACKDROP} for ${Util.describe(context)}`) + } + return backdrop + } + + // Find closest bmd-layout-container based on the given context + findContainer(raiseError = true, context = this.$element) { + let container = context.closest(Selector.CONTAINER) + if (container.length === 0 && raiseError) { + $.error(`Failed to find ${Selector.CONTAINER} for ${Util.describe(context)}`) + } + return container + } + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + + } + + return BaseLayout + +})(jQuery) + +export default BaseLayout diff --git a/templates/assets/bootstrap-material-design/js/baseSelection.js b/templates/assets/bootstrap-material-design/js/baseSelection.js new file mode 100644 index 0000000..765d1c3 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/baseSelection.js @@ -0,0 +1,93 @@ +import BaseInput from './baseInput' +import Util from './util' + +const BaseSelection = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const Default = { + 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 + //] + } + } + + const Selector = { + LABEL: 'label' + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class BaseSelection extends BaseInput { + + constructor($element, config, properties) { + // properties = {inputType: checkbox, outerClass: checkbox-inline} + // '.checkbox|switch|radio > label > input[type=checkbox|radio]' + // '.${this.outerClass} > label > input[type=${this.inputType}]' + + super($element, $.extend(true, {}, Default, config), properties) + this.decorateMarkup() + } + + // ------------------------------------------------------------------------ + // protected + + decorateMarkup() { + this.$element.after(this.config.template) + } + + // Demarcation element (e.g. first child of a form-group) + outerElement() { + // .checkbox|switch|radio > label > input[type=checkbox|radio] + // label.checkbox-inline > input[type=checkbox|radio] + // .${this.outerClass} > label > input[type=${this.inputType}] + return this.$element.parent().closest(`.${this.outerClass}`) + } + + rejectWithoutRequiredStructure() { + // '.checkbox|switch|radio > label > input[type=checkbox|radio]' + // '.${this.outerClass} > label > input[type=${this.inputType}]' + Util.assert(this.$element, !this.$element.parent().prop('tagName') === 'label', `${this.constructor.name}'s ${Util.describe(this.$element)} parent element should be <label>.`) + Util.assert(this.$element, !this.outerElement().hasClass(this.outerClass), `${this.constructor.name}'s ${Util.describe(this.$element)} outer element should have class ${this.outerClass}.`) + } + + addFocusListener() { + // checkboxes didn't appear to bubble to the document, so we'll bind these directly + this.$element.closest(Selector.LABEL).hover(() => { + this.addFormGroupFocus() + }, () => { + this.removeFormGroupFocus() + }) + } + + addChangeListener() { + this.$element.change(() => { + this.$element.blur() + }) + } + + // ------------------------------------------------------------------------ + // private + } + + return BaseSelection + +})(jQuery) + +export default BaseSelection diff --git a/templates/assets/bootstrap-material-design/js/bootstrapMaterialDesign.js b/templates/assets/bootstrap-material-design/js/bootstrapMaterialDesign.js new file mode 100644 index 0000000..b3be531 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/bootstrapMaterialDesign.js @@ -0,0 +1,205 @@ +/** + * $.bootstrapMaterialDesign(config) is a macro class to configure the components generally + * used in Bootstrap Material Design. 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. + */ +const BootstrapMaterialDesign = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'bootstrapMaterialDesign' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = NAME // retain this full name since it is long enough not to conflict + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + /** + * Global configuration: + * The global configuration hash will be mixed in to each components' config. + * e.g. calling $.bootstrapMaterialDesign({global: { validate: true } }) would pass `validate:true` to every component + * + * + * Component configuration: + * - selector: may be a string or an array. Any array will be joined with a comma to generate the selector + * - disable any component by defining it as false with an override. e.g. $.bootstrapMaterialDesign({ autofill: false }) + * + * @see each individual component for more configuration settings. + */ + const Default = { + global: { + validate: false, + label: { + className: 'bmd-label-static' // default style of label to be used if not specified in the html markup + } + }, + autofill: { + selector: 'body' + }, + checkbox: { + selector: '.checkbox > label > input[type=checkbox]' + }, + checkboxInline: { + selector: 'label.checkbox-inline > input[type=checkbox]' + }, + collapseInline: { + selector: '.bmd-collapse-inline [data-toggle="collapse"]' + }, + drawer: { + selector: '.bmd-layout-drawer' + }, + file: { + selector: 'input[type=file]' + }, + radio: { + selector: '.radio > label > input[type=radio]' + }, + radioInline: { + selector: 'label.radio-inline > input[type=radio]' + }, + ripples: { + //selector: ['.btn:not(.btn-link):not(.ripple-none)'] // testing only + selector: [ + '.btn:not(.btn-link):not(.ripple-none)', + '.card-image:not(.ripple-none)', + '.navbar a:not(.ripple-none)', + '.dropdown-menu a:not(.ripple-none)', + '.nav-tabs a:not(.ripple-none)', + '.pagination li:not(.active):not(.disabled) a:not(.ripple-none)', + '.ripple' // generic marker class to add ripple to elements + ] + }, + select: { + selector: ['select'] + }, + switch: { + selector: '.switch > label > input[type=checkbox]' + }, + text: { + // omit inputs we have specialized components to handle - we need to match text, email, etc. The easiest way to do this appears to be just omit the ones we don't want to match and let the rest fall through to this. + selector: [`input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=file]):not([type=button]):not([type=submit]):not([type=reset])`] + }, + textarea: { + selector: ['textarea'] + }, + arrive: true, + // create an ordered component list for instantiation + instantiation: [ + 'ripples', + 'checkbox', + 'checkboxInline', + 'collapseInline', + 'drawer', + //'file', + 'radio', + 'radioInline', + 'switch', + 'text', + 'textarea', + 'select', + 'autofill' + ] + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class BootstrapMaterialDesign { + + constructor($element, config) { + this.$element = $element + this.config = $.extend(true, {}, Default, config) + let $document = $(document) + + for (let component of this.config.instantiation) { + + // the component's config fragment is passed in directly, allowing users to override + let componentConfig = this.config[component] + + // check to make sure component config is enabled (not `false`) + if (componentConfig) { + + // assemble the selector as it may be an array + let selector = this._resolveSelector(componentConfig) + + // mix in global options + componentConfig = $.extend(true, {}, this.config.global, componentConfig) + + // create the jquery fn name e.g. 'bmdText' for 'text' + let componentName = `${component.charAt(0).toUpperCase() + component.slice(1)}` + let jqueryFn = `bmd${componentName}` + + try { + // safely instantiate component on selector elements with config, report errors and move on. + // console.debug(`instantiating: $('${selector}')[${jqueryFn}](${componentConfig})`) // eslint-disable-line no-console + $(selector)[jqueryFn](componentConfig) + + // add to arrive if present and enabled + if (document.arrive && this.config.arrive) { + $document.arrive(selector, function(){ // eslint-disable-line no-loop-func + $(this)[jqueryFn](componentConfig) + }) + } + } catch (e) { + let message = `Failed to instantiate component: $('${selector}')[${jqueryFn}](${componentConfig})` + console.error(message, e, `\nSelected elements: `, $(selector)) // eslint-disable-line no-console + throw e + } + } + } + } + + dispose() { + this.$element.data(DATA_KEY, null) + this.$element = null + this.config = null + } + + // ------------------------------------------------------------------------ + // private + + _resolveSelector(componentConfig) { + let selector = componentConfig.selector + if (Array.isArray(selector)) { + selector = selector.join(', ') + } + + return selector + } + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new BootstrapMaterialDesign($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = BootstrapMaterialDesign._jQueryInterface + $.fn[JQUERY_NAME].Constructor = BootstrapMaterialDesign + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return BootstrapMaterialDesign._jQueryInterface + } + + return BootstrapMaterialDesign + +})(jQuery) + +export default BootstrapMaterialDesign diff --git a/templates/assets/bootstrap-material-design/js/checkbox.js b/templates/assets/bootstrap-material-design/js/checkbox.js new file mode 100644 index 0000000..9b93f40 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/checkbox.js @@ -0,0 +1,94 @@ +import BaseSelection from './baseSelection' +//import Text from './text' +//import File from './file' +//import Radio from './radio' +//import Textarea from './textarea' +//import Select from './select' +import Util from './util' + +const Checkbox = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'checkbox' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + template: `<span class='checkbox-decorator'><span class='check'></span></span>` + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Checkbox extends BaseSelection { + + constructor($element, config, properties = {inputType: NAME, outerClass: NAME}) { + super($element, $.extend(true, + //{invalidComponentMatches: [File, Radio, Text, Textarea, Select]}, + Default, config), properties) + } + + dispose(dataKey = DATA_KEY) { + super.dispose(dataKey) + } + + static matches($element) { + // '.checkbox > label > input[type=checkbox]' + if ($element.attr('type') === 'checkbox') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='checkbox'.`) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Checkbox($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Checkbox._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Checkbox + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Checkbox._jQueryInterface + } + + return Checkbox + +})(jQuery) + +export default Checkbox diff --git a/templates/assets/bootstrap-material-design/js/checkboxInline.js b/templates/assets/bootstrap-material-design/js/checkboxInline.js new file mode 100644 index 0000000..4ad32d0 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/checkboxInline.js @@ -0,0 +1,89 @@ +import Checkbox from './checkbox' + +const CheckboxInline = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'checkboxInline' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + bmdFormGroup: { + create: false, // no bmd-form-group creation if form-group not present. It messes with the layout. + required: false + } + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class CheckboxInline extends Checkbox { + + constructor($element, config, properties = {inputType: 'checkbox', outerClass: 'checkbox-inline'}) { + super($element, $.extend(true, {}, Default, config), properties) + } + + dispose() { + super.dispose(DATA_KEY) + } + + //static matches($element) { + // // '.checkbox-inline > input[type=checkbox]' + // if ($element.attr('type') === 'checkbox') { + // return true + // } + // return false + //} + // + //static rejectMatch(component, $element) { + // Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='checkbox'.`) + //} + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new CheckboxInline($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = CheckboxInline._jQueryInterface + $.fn[JQUERY_NAME].Constructor = CheckboxInline + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return CheckboxInline._jQueryInterface + } + + return CheckboxInline + +})(jQuery) + +export default CheckboxInline diff --git a/templates/assets/bootstrap-material-design/js/collapseInline.js b/templates/assets/bootstrap-material-design/js/collapseInline.js new file mode 100644 index 0000000..10d813f --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/collapseInline.js @@ -0,0 +1,113 @@ +import Base from './base' +import Util from './util' + +const CollapseInline = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'collapseInline' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Selector = { + ANY_INPUT: 'input, select, textarea' + } + + const ClassName = { + IN: 'in', + COLLAPSE: 'collapse', + COLLAPSING: 'collapsing', + COLLAPSED: 'collapsed', + WIDTH: 'width' + } + const Default = {} + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class CollapseInline extends Base { + + // $element is expected to be the trigger + // i.e. <button class="btn bmd-btn-icon" for="search" data-toggle="collapse" data-target="#search-field" aria-expanded="false" aria-controls="search-field"> + constructor($element, config) { + super($element, $.extend(true, {}, Default, config)) + this.$bmdFormGroup = this.findMdbFormGroup(true) + + let collapseSelector = $element.data('target') + this.$collapse = $(collapseSelector) + + Util.assert($element, this.$collapse.length === 0, `Cannot find collapse target for ${Util.describe($element)}`) + Util.assert(this.$collapse, !this.$collapse.hasClass(ClassName.COLLAPSE), `${Util.describe(this.$collapse)} is expected to have the '${ClassName.COLLAPSE}' class. It is being targeted by ${Util.describe($element)}`) + + // find the first input for focusing + let $inputs = this.$bmdFormGroup.find(Selector.ANY_INPUT) + if ($inputs.length > 0) { + this.$input = $inputs.first() + } + + // automatically add the marker class to collapse width instead of height - nice convenience because it is easily forgotten + if (!this.$collapse.hasClass(ClassName.WIDTH)) { + this.$collapse.addClass(ClassName.WIDTH) + } + + if (this.$input) { + // add a listener to set focus + this.$collapse.on('shown.bs.collapse', () => { + this.$input.focus() + }) + + // add a listener to collapse field + this.$input.blur(() => { + this.$collapse.collapse('hide') + }) + } + } + + dispose() { + super.dispose(DATA_KEY) + this.$bmdFormGroup = null + this.$collapse = null + this.$input = null + } + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new CollapseInline($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = CollapseInline._jQueryInterface + $.fn[JQUERY_NAME].Constructor = CollapseInline + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return CollapseInline._jQueryInterface + } + + return CollapseInline + +})(jQuery) + +export default CollapseInline diff --git a/templates/assets/bootstrap-material-design/js/drawer.js b/templates/assets/bootstrap-material-design/js/drawer.js new file mode 100644 index 0000000..ef0586e --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/drawer.js @@ -0,0 +1,170 @@ +import BaseLayout from './baseLayout' + +const Drawer = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'drawer' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Keycodes = { + ESCAPE: 27 + //ENTER: 13, + //SPACE: 32 + } + + const ClassName = { + IN: 'in', + DRAWER_IN: `bmd-drawer-in`, + DRAWER_OUT: `bmd-drawer-out`, + DRAWER: 'bmd-layout-drawer', + CONTAINER: 'bmd-layout-container' + } + + const Default = { + focusSelector: `a, button, input` + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Drawer extends BaseLayout { + + // $element is expected to be the trigger + // i.e. <button class="btn bmd-btn-icon" for="search" data-toggle="drawer" data-target="#my-side-nav-drawer" aria-expanded="false" aria-controls="my-side-nav-drawer"> + constructor($element, config) { + super($element, $.extend(true, {}, Default, config)) + + this.$toggles = $(`[data-toggle="drawer"][href="#${this.$element[0].id}"], [data-toggle="drawer"][data-target="#${this.$element[0].id}"]`) + + this._addAria() + + // click or escape on the backdrop closes the drawer + this.$backdrop.keydown((ev) => { + if (ev.which === Keycodes.ESCAPE) { + this.hide() + } + }).click(() => { + this.hide() + }) + + // escape on the drawer closes it + this.$element.keydown((ev) => { + if (ev.which === Keycodes.ESCAPE) { + this.hide() + } + }) + + // any toggle button clicks + this.$toggles.click(() => { + this.toggle() + }) + } + + dispose() { + super.dispose(DATA_KEY) + this.$toggles = null + } + + toggle() { + if (this._isOpen()) { + this.hide() + } else { + this.show() + } + } + + show() { + if (this._isForcedClosed() || this._isOpen()) { + return + } + + this.$toggles.attr('aria-expanded', true) + this.$element.attr('aria-expanded', true) + this.$element.attr('aria-hidden', false) + + // focus on the first focusable item + let $focusOn = this.$element.find(this.config.focusSelector) + if ($focusOn.length > 0) { + $focusOn.first().focus() + } + + this.$container.addClass(ClassName.DRAWER_IN) + // backdrop is responsively styled based on bmd-drawer-overlay, therefore style is none of our concern, simply add the marker class and let the scss determine if it should be displayed or not. + this.$backdrop.addClass(ClassName.IN) + } + + hide() { + if (!this._isOpen()) { + return + } + + this.$toggles.attr('aria-expanded', false) + this.$element.attr('aria-expanded', false) + this.$element.attr('aria-hidden', true) + + this.$container.removeClass(ClassName.DRAWER_IN) + this.$backdrop.removeClass(ClassName.IN) + } + + + // ------------------------------------------------------------------------ + // private + + _isOpen() { + return this.$container.hasClass(ClassName.DRAWER_IN) + } + + _isForcedClosed() { + return this.$container.hasClass(ClassName.DRAWER_OUT) + } + + _addAria() { + let isOpen = this._isOpen() + this.$element.attr('aria-expanded', isOpen) + this.$element.attr('aria-hidden', isOpen) + + if (this.$toggles.length) { + this.$toggles.attr('aria-expanded', isOpen) + } + } + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Drawer($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Drawer._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Drawer + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Drawer._jQueryInterface + } + + return Drawer + +})(jQuery) + +export default Drawer diff --git a/templates/assets/bootstrap-material-design/js/file.js b/templates/assets/bootstrap-material-design/js/file.js new file mode 100644 index 0000000..ba75a93 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/file.js @@ -0,0 +1,139 @@ +import BaseInput from './baseInput' +//import Checkbox from './checkbox' +//import Radio from './radio' +//import Switch from './switch' +//import Text from './text' +//import Textarea from './textarea' +//import Select from './select' +import Util from './util' + +const File = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'file' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = {} + + const ClassName = { + FILE: NAME, + IS_FILE: 'is-file' + } + + const Selector = { + FILENAMES: 'input.form-control[readonly]' + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class File extends BaseInput { + + constructor($element, config) { + super($element, $.extend(true, + //{invalidComponentMatches: [Checkbox, Radio, Text, Textarea, Select, Switch]}, + Default, config)) + + this.$bmdFormGroup.addClass(ClassName.IS_FILE) + } + + dispose() { + super.dispose(DATA_KEY) + } + + static matches($element) { + if ($element.attr('type') === 'file') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='file'.`) + } + + // ------------------------------------------------------------------------ + // protected + + // Demarcation element (e.g. first child of a form-group) + outerElement() { + // label.file > input[type=file] + return this.$element.parent().closest(`.${ClassName.FILE}`) + } + + rejectWithoutRequiredStructure() { + // label.file > input[type=file] + Util.assert(this.$element, !this.outerElement().prop('tagName') === 'label', `${this.constructor.name}'s ${Util.describe(this.$element)} parent element ${Util.describe(this.outerElement())} should be <label>.`) + Util.assert(this.$element, !this.outerElement().hasClass(ClassName.FILE), `${this.constructor.name}'s ${Util.describe(this.$element)} parent element ${Util.describe(this.outerElement())} should have class .${ClassName.FILE}.`) + } + + addFocusListener() { + this.$bmdFormGroup + .on('focus', () => { + this.addFormGroupFocus() + }) + .on('blur', () => { + this.removeFormGroupFocus() + }) + } + + addChangeListener() { + // set the fileinput readonly field with the name of the file + this.$element.on('change', () => { + let value = '' + $.each(this.$element.files, (i, file) => { + value += `${file.name} , ` + }) + value = value.substring(0, value.length - 2) + if (value) { + this.addIsFilled() + } else { + this.removeIsFilled() + } + this.$bmdFormGroup.find(Selector.FILENAMES).val(value) + }) + } + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new File($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = File._jQueryInterface + $.fn[JQUERY_NAME].Constructor = File + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return File._jQueryInterface + } + + return File + +})(jQuery) + +export default File diff --git a/templates/assets/bootstrap-material-design/js/index.js b/templates/assets/bootstrap-material-design/js/index.js new file mode 100644 index 0000000..b90a2c0 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/index.js @@ -0,0 +1,32 @@ +/* + * This is the main entry point. + * + * You can import other modules here, including external packages. When bundling using rollup you can mark those modules as external and have them excluded or, if they have a jsnext:main entry in their package.json (like this package does), let rollup bundle them into your dist file. + * + * IMPORTANT NOTE: If you are ultimately creating an iife/self executing bundle for the browser, be sure to: + * import 'babel-polyfill' + * + * at your application entry point. This is necessary for browsers that do not yet support some ES2015 runtime necessities such as Symbol. We do this in `index-iife.js` for our iife rollup bundle. + */ +import 'babel-polyfill' + +// Bootstrap components +import 'bootstrap/dist/js/bootstrap' + +// invalidComponentMatches is currently disabled due to https://github.com/rollup/rollup/issues/428#issuecomment-170066452 +import './checkbox' +import './checkboxInline' +import './collapseInline' +import './file' +import './radio' +import './radioInline' +import './select' +import './switch' +import './text' +import './textarea' + +import './drawer' + +import './ripples' +import './autofill' +import './bootstrapMaterialDesign' diff --git a/templates/assets/bootstrap-material-design/js/radio.js b/templates/assets/bootstrap-material-design/js/radio.js new file mode 100644 index 0000000..8ceb6a9 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/radio.js @@ -0,0 +1,95 @@ +import BaseSelection from './baseSelection' +//import Text from './text' +//import File from './file' +//import Checkbox from './checkbox' +//import Switch from './switch' +import Util from './util' + +const Radio = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'radio' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + template: `<span class='bmd-radio-outer-circle'></span><span class='bmd-radio-inner-circle'></span>` + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Radio extends BaseSelection { + + constructor($element, config, properties = {inputType: NAME, outerClass: NAME}) { + super($element, $.extend(true, + //{invalidComponentMatches: [Checkbox, File, Switch, Text]}, + Default, config), properties) + } + + dispose(dataKey = DATA_KEY) { + super.dispose(dataKey) + } + + static matches($element) { + // '.radio > label > input[type=radio]' + if ($element.attr('type') === 'radio') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='radio'.`) + } + + // ------------------------------------------------------------------------ + // protected + + //decorateMarkup() { + // this.$element.after(this.config.template) + //} + + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Radio($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Radio._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Radio + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Radio._jQueryInterface + } + + return Radio + +})(jQuery) + +export default Radio diff --git a/templates/assets/bootstrap-material-design/js/radioInline.js b/templates/assets/bootstrap-material-design/js/radioInline.js new file mode 100644 index 0000000..679c303 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/radioInline.js @@ -0,0 +1,77 @@ +import Radio from './radio' + +const RadioInline = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'radioInline' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + bmdFormGroup: { + create: false, // no bmd-form-group creation if form-group not present. It messes with the layout. + required: false + } + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class RadioInline extends Radio { + + constructor($element, config, properties = {inputType: 'radio', outerClass: 'radio-inline'}) { + super($element, $.extend(true, {}, Default, config), properties) + } + + dispose() { + super.dispose(DATA_KEY) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new RadioInline($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = RadioInline._jQueryInterface + $.fn[JQUERY_NAME].Constructor = RadioInline + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return RadioInline._jQueryInterface + } + + return RadioInline + +})(jQuery) + +export default RadioInline diff --git a/templates/assets/bootstrap-material-design/js/ripples.js b/templates/assets/bootstrap-material-design/js/ripples.js new file mode 100644 index 0000000..c28d287 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/ripples.js @@ -0,0 +1,311 @@ +import Util from './util' + +const Ripples = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'ripples' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const ClassName = { + CONTAINER: 'ripple-container', + DECORATOR: 'ripple-decorator' + } + + const Selector = { + CONTAINER: `.${ClassName.CONTAINER}`, + DECORATOR: `.${ClassName.DECORATOR}` //, + } + + + const Default = { + container: { + template: `<div class='${ClassName.CONTAINER}'></div>` + }, + decorator: { + template: `<div class='${ClassName.DECORATOR}'></div>` + }, + trigger: { + start: 'mousedown touchstart', + end: 'mouseup mouseleave touchend' + }, + touchUserAgentRegex: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i, + duration: 500 + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Ripples { + + constructor($element, config) { + this.$element = $element + + //console.log(`Adding ripples to ${Util.describe(this.$element)}`) // eslint-disable-line no-console + this.config = $.extend(true, {}, Default, config) + + // attach initial listener + this.$element.on(this.config.trigger.start, (event) => { + this._onStartRipple(event) + }) + } + + + dispose() { + this.$element.data(DATA_KEY, null) + this.$element = null + this.$container = null + this.$decorator = null + this.config = null + } + + // ------------------------------------------------------------------------ + // private + + _onStartRipple(event) { + + // Verify if the user is just touching on a device and return if so + if (this._isTouch() && event.type === 'mousedown') { + return + } + + // Find or create the ripple container element + this._findOrCreateContainer() + + // Get relY and relX positions of the container element + let relY = this._getRelY(event) + let relX = this._getRelX(event) + + // If relY and/or relX are false, return the event + if (!relY && !relX) { + return + } + + // set the location and color each time (even if element is cached) + this.$decorator.css({ + left: relX, + top: relY, + 'background-color': this._getRipplesColor() + }) + + // Make sure the ripple has the styles applied (ugly hack but it works) + this._forceStyleApplication() + + // Turn on the ripple animation + this.rippleOn() + + // Call the rippleEnd function when the transition 'on' ends + setTimeout(() => { + this.rippleEnd() + }, this.config.duration) + + // Detect when the user leaves the element to cleanup if not already done? + this.$element.on(this.config.trigger.end, () => { + if (this.$decorator) { // guard against race condition/mouse attack + this.$decorator.data('mousedown', 'off') + + if (this.$decorator.data('animating') === 'off') { + this.rippleOut() + } + } + }) + } + + _findOrCreateContainer() { + if (!this.$container || !this.$container.length > 0) { + this.$element.append(this.config.container.template) + this.$container = this.$element.find(Selector.CONTAINER) + } + + // always add the rippleElement, it is always removed + this.$container.append(this.config.decorator.template) + this.$decorator = this.$container.find(Selector.DECORATOR) + } + + // Make sure the ripple has the styles applied (ugly hack but it works) + _forceStyleApplication() { + return window.getComputedStyle(this.$decorator[0]).opacity + } + + + /** + * Get the relX + */ + _getRelX(event) { + let wrapperOffset = this.$container.offset() + + let result = null + if (!this._isTouch()) { + // Get the mouse position relative to the ripple wrapper + result = event.pageX - wrapperOffset.left + } else { + // Make sure the user is using only one finger and then get the touch + // position relative to the ripple wrapper + event = event.originalEvent + + if (event.touches.length === 1) { + result = event.touches[0].pageX - wrapperOffset.left + } else { + result = false + } + } + + return result + } + + /** + * Get the relY + */ + _getRelY(event) { + let containerOffset = this.$container.offset() + let result = null + + if (!this._isTouch()) { + /** + * Get the mouse position relative to the ripple wrapper + */ + result = event.pageY - containerOffset.top + } else { + /** + * Make sure the user is using only one finger and then get the touch + * position relative to the ripple wrapper + */ + event = event.originalEvent + + if (event.touches.length === 1) { + result = event.touches[0].pageY - containerOffset.top + } else { + result = false + } + } + + return result + } + + /** + * Get the ripple color + */ + _getRipplesColor() { + let color = this.$element.data('ripple-color') ? this.$element.data('ripple-color') : window.getComputedStyle(this.$element[0]).color + return color + } + + /** + * Verify if the client is using a mobile device + */ + _isTouch() { + return this.config.touchUserAgentRegex.test(navigator.userAgent) + } + + /** + * End the animation of the ripple + */ + rippleEnd() { + if (this.$decorator) { // guard against race condition/mouse attack + this.$decorator.data('animating', 'off') + + if (this.$decorator.data('mousedown') === 'off') { + this.rippleOut(this.$decorator) + } + } + } + + /** + * Turn off the ripple effect + */ + rippleOut() { + this.$decorator.off() + + if (Util.transitionEndSupported()) { + this.$decorator.addClass('ripple-out') + } else { + this.$decorator.animate({opacity: 0}, 100, () => { + this.$decorator.trigger('transitionend') + }) + } + + this.$decorator.on(Util.transitionEndSelector(), () => { + if (this.$decorator) { + this.$decorator.remove() + this.$decorator = null + } + }) + } + + /** + * Turn on the ripple effect + */ + rippleOn() { + let size = this._getNewSize() + + if (Util.transitionEndSupported()) { + this.$decorator + .css({ + '-ms-transform': `scale(${size})`, + '-moz-transform': `scale(${size})`, + '-webkit-transform': `scale(${size})`, + transform: `scale(${size})` + }) + .addClass('ripple-on') + .data('animating', 'on') + .data('mousedown', 'on') + } else { + this.$decorator.animate({ + width: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, + height: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, + 'margin-left': Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * (-1), + 'margin-top': Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * (-1), + opacity: 0.2 + }, this.config.duration, () => { + this.$decorator.trigger('transitionend') + }) + } + } + + /** + * Get the new size based on the element height/width and the ripple width + */ + _getNewSize() { + return (Math.max(this.$element.outerWidth(), this.$element.outerHeight()) / this.$decorator.outerWidth()) * 2.5 + } + + // ------------------------------------------------------------------------ + // static + + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Ripples($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Ripples._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Ripples + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Ripples._jQueryInterface + } + + return Ripples + +})(jQuery) + +export default Ripples diff --git a/templates/assets/bootstrap-material-design/js/select.js b/templates/assets/bootstrap-material-design/js/select.js new file mode 100644 index 0000000..765f411 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/select.js @@ -0,0 +1,94 @@ +import BaseFormControl from './baseFormControl' +//import Checkbox from './checkbox' +//import File from './file' +//import Radio from './radio' +//import Switch from './switch' +//import Text from './text' +//import Textarea from './textarea' +import Util from './util' + +const Select = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'select' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + requiredClasses: ['form-control||custom-select'] + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Select extends BaseFormControl { + + constructor($element, config) { + super($element, $.extend(true, + //{invalidComponentMatches: [Checkbox, File, Radio, Switch, Text, Textarea]}, + Default, config)) + + // floating labels will cover the options, so trigger them to be above (if used) + this.addIsFilled() + } + + dispose() { + super.dispose(DATA_KEY) + } + + static matches($element) { + if ($element.prop('tagName') === 'select') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for <select>.`) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Select($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Select._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Select + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Select._jQueryInterface + } + + return Select + +})(jQuery) + +export default Select diff --git a/templates/assets/bootstrap-material-design/js/switch.js b/templates/assets/bootstrap-material-design/js/switch.js new file mode 100644 index 0000000..cad4806 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/switch.js @@ -0,0 +1,72 @@ +import Checkbox from './checkbox' + +const Switch = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'switch' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = { + template: `<span class='bmd-switch-track'></span>` + } + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Switch extends Checkbox { + + constructor($element, config, properties = {inputType: 'checkbox', outerClass: 'switch'}) { + super($element, $.extend(true, {}, Default, config), properties) + // selector: '.switch > label > input[type=checkbox]' + } + + dispose() { + super.dispose(DATA_KEY) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Switch($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Switch._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Switch + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Switch._jQueryInterface + } + + return Switch + +})(jQuery) + +export default Switch diff --git a/templates/assets/bootstrap-material-design/js/text.js b/templates/assets/bootstrap-material-design/js/text.js new file mode 100644 index 0000000..f9bbd12 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/text.js @@ -0,0 +1,89 @@ +import BaseFormControl from './baseFormControl' +//import Checkbox from './checkbox' +//import File from './file' +//import Radio from './radio' +//import Switch from './switch' +//import Textarea from './textarea' +//import Select from './select' +import Util from './util' + +const Text = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'text' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = {} + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Text extends BaseFormControl { + + constructor($element, config) { + super($element, $.extend(true, + //{invalidComponentMatches: [Checkbox, File, Radio, Switch, Select, Textarea]}, + Default, config)) + } + + dispose(dataKey = DATA_KEY) { + super.dispose(dataKey) + } + + static matches($element) { + if ($element.attr('type') === 'text') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='text'.`) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Text($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Text._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Text + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Text._jQueryInterface + } + + return Text + +})(jQuery) + +export default Text diff --git a/templates/assets/bootstrap-material-design/js/textarea.js b/templates/assets/bootstrap-material-design/js/textarea.js new file mode 100644 index 0000000..d5596a0 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/textarea.js @@ -0,0 +1,89 @@ +import BaseFormControl from './baseFormControl' +//import Checkbox from './checkbox' +//import File from './file' +//import Radio from './radio' +//import Switch from './switch' +//import Text from './text' +//import Select from './select' +import Util from './util' + +const Textarea = (($) => { + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + const NAME = 'textarea' + const DATA_KEY = `bmd.${NAME}` + const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` + const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] + + const Default = {} + + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + class Textarea extends BaseFormControl { + + constructor($element, config) { + super($element, $.extend(true, + //{invalidComponentMatches: [Checkbox, File, Radio, Text, Select, Switch]}, + Default, config)) + } + + dispose() { + super.dispose(DATA_KEY) + } + + static matches($element) { + if ($element.prop('tagName') === 'textarea') { + return true + } + return false + } + + static rejectMatch(component, $element) { + Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for <textarea>.`) + } + + // ------------------------------------------------------------------------ + // protected + + // ------------------------------------------------------------------------ + // private + + // ------------------------------------------------------------------------ + // static + static _jQueryInterface(config) { + return this.each(function () { + let $element = $(this) + let data = $element.data(DATA_KEY) + + if (!data) { + data = new Textarea($element, config) + $element.data(DATA_KEY, data) + } + }) + } + } + + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + $.fn[JQUERY_NAME] = Textarea._jQueryInterface + $.fn[JQUERY_NAME].Constructor = Textarea + $.fn[JQUERY_NAME].noConflict = () => { + $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT + return Textarea._jQueryInterface + } + + return Textarea + +})(jQuery) + +export default Textarea diff --git a/templates/assets/bootstrap-material-design/js/util.js b/templates/assets/bootstrap-material-design/js/util.js new file mode 100644 index 0000000..3cd2e77 --- /dev/null +++ b/templates/assets/bootstrap-material-design/js/util.js @@ -0,0 +1,105 @@ +const Util = (() => { + + /** + * ------------------------------------------------------------------------ + * Private TransitionEnd Helpers + * ------------------------------------------------------------------------ + */ + + let transitionEnd = false + let transitionEndSelector = '' + + const TransitionEndEvent = { + WebkitTransition: 'webkitTransitionEnd', + MozTransition: 'transitionend', + OTransition: 'oTransitionEnd otransitionend', + transition: 'transitionend' + } + + function transitionEndTest() { + if (window.QUnit) { + return false + } + + let el = document.createElement('bmd') + + for (let name in TransitionEndEvent) { + if (el.style[name] !== undefined) { + return TransitionEndEvent[name] // { end: TransitionEndEvent[name] } + } + } + + return false + } + + function setTransitionEndSupport() { + transitionEnd = transitionEndTest() + + // generate a concatenated transition end event selector + for (let name in TransitionEndEvent) { + transitionEndSelector += ` ${TransitionEndEvent[name]}` + } + } + + /** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ + + let Util = { + + transitionEndSupported() { + return transitionEnd + }, + + transitionEndSelector() { + return transitionEndSelector + }, + + isChar(event) { + if (typeof event.which === 'undefined') { + return true + } else if (typeof event.which === 'number' && event.which > 0) { + return ( + !event.ctrlKey + && !event.metaKey + && !event.altKey + && event.which !== 8 // backspace + && event.which !== 9 // tab + && event.which !== 13 // enter + && event.which !== 16 // shift + && event.which !== 17 // ctrl + && event.which !== 20 // caps lock + && event.which !== 27 // escape + ) + } + return false + }, + + assert($element, invalidTest, message) { + if (invalidTest) { + if (!$element === undefined) { + $element.css('border', '1px solid red') + } + console.error(message, $element) // eslint-disable-line no-console + throw message + } + }, + + describe($element) { + if ($element === undefined) { + return 'undefined' + } else if ($element.length === 0) { + return '(no matching elements)' + } + return `${$element[0].outerHTML.split('>')[0]}>` + } + } + + setTransitionEndSupport() + return Util + +})(jQuery) + +export default Util |
