/* * Form Widget * * Dependences: * - Nil */ +function ($) { "use strict"; var Base = $.wn.foundation.base, BaseProto = Base.prototype var FormWidget = function (element, options) { this.$el = $(element) this.options = options || {} this.fieldElementCache = null /* * Throttle dependency updating */ this.dependantUpdateInterval = 300 this.dependantUpdateTimers = {} $.wn.foundation.controlUtils.markDisposable(element) Base.call(this) this.init() } FormWidget.prototype = Object.create(BaseProto) FormWidget.prototype.constructor = FormWidget FormWidget.prototype.init = function() { this.$form = this.$el.closest('form') this.bindDependants() this.bindCheckboxlist() this.toggleEmptyTabs() this.bindLazyTabs() this.bindCollapsibleSections() this.$el.on('oc.triggerOn.afterUpdate', this.proxy(this.toggleEmptyTabs)) this.$el.one('dispose-control', this.proxy(this.dispose)) } FormWidget.prototype.dispose = function() { this.unbindDependants() this.unbindCheckboxList() this.unbindLazyTabs() this.unbindCollapsibleSections() this.$el.off('dispose-control', this.proxy(this.dispose)) this.$el.removeData('oc.formwidget') this.$el = null this.$form = null this.options = null this.fieldElementCache = null BaseProto.dispose.call(this) } /* * Logic for checkboxlist */ FormWidget.prototype.bindCheckboxlist = function() { var checkAllBoxes = function($field, flag) { $('input[type=checkbox]', $field) .prop('checked', flag) .first() .trigger('change') } this.$el.on('click', '[data-field-checkboxlist-all]', function() { checkAllBoxes($(this).closest('.field-checkboxlist'), true) }) this.$el.on('click', '[data-field-checkboxlist-none]', function() { checkAllBoxes($(this).closest('.field-checkboxlist'), false) }) } /* * Unbind checkboxlist handlers */ FormWidget.prototype.unbindCheckboxList = function() { this.$el.off('click', '[data-field-checkboxlist-all]') this.$el.off('click', '[data-field-checkboxlist-none]') } /* * Get all fields elements that belong to this form, nested form * fields are removed from this collection. */ FormWidget.prototype.getFieldElements = function() { if (this.fieldElementCache !== null) { return this.fieldElementCache } var form = this.$el, nestedFields = form.find('[data-control="formwidget"] [data-field-name]') return this.fieldElementCache = form.find('[data-field-name]').not(nestedFields) } /* * Bind dependant fields */ FormWidget.prototype.bindDependants = function() { var self = this, fieldMap = this._getDependants() /* * When a field is updated, refresh its dependents */ $.each(fieldMap, function(fieldName, toRefresh) { $(document).on('change.oc.formwidget', '[data-field-name="' + fieldName + '"]', $.proxy(self.onRefreshDependants, self, fieldName, toRefresh) ) }) } /* * Dispose of the dependant field handlers */ FormWidget.prototype.unbindDependants = function() { var fieldMap = this._getDependants() $.each(fieldMap, function(fieldName, toRefresh) { $(document).off('change.oc.formwidget', '[data-field-name="' + fieldName + '"]') }) } /* * Retrieve the dependant fields */ FormWidget.prototype._getDependants = function() { if (!$('[data-field-depends]', this.$el).length) { return; } var fieldMap = {}, fieldElements = this.getFieldElements() /* * Map master and slave fields */ fieldElements.filter('[data-field-depends]').each(function() { var name = $(this).data('field-name'), depends = $(this).data('field-depends') $.each(depends, function(index, depend){ if (!fieldMap[depend]) { fieldMap[depend] = { fields: [] } } fieldMap[depend].fields.push(name) }) }) return fieldMap } /* * Refresh a dependancy field * Uses a throttle to prevent duplicate calls and click spamming. * * The event parameter is passed automatically by jQuery as the third * argument (after the two preset arguments from $.proxy). It carries * a cascadeChain array that tracks which fields have already been * refreshed in the current cascade, preventing infinite loops when * fields have circular dependsOn declarations. */ FormWidget.prototype.onRefreshDependants = function(fieldName, toRefresh, event) { var self = this, form = this.$el, formEl = this.$form, fieldElements = this.getFieldElements(), cascadeChain = (event && event.cascadeChain) || [] /* * If this field already appears in the cascade chain, we have * a circular dependency. Stop the cascade to prevent an * infinite loop. See: https://github.com/wintercms/winter/issues/421 */ if (cascadeChain.indexOf(fieldName) !== -1) { return } if (this.dependantUpdateTimers[fieldName] !== undefined) { window.clearTimeout(this.dependantUpdateTimers[fieldName]) } this.dependantUpdateTimers[fieldName] = window.setTimeout(function() { var refreshData = $.extend({}, toRefresh, paramToObj('data-refresh-data', self.options.refreshData) ) formEl.request(self.options.refreshHandler, { data: refreshData }).success(function() { self.toggleEmptyTabs() var newChain = cascadeChain.concat([fieldName]) $.each(toRefresh.fields, function(key, field) { var cascadeEvent = $.Event('change') cascadeEvent.cascadeChain = newChain $('[data-field-name="' + field + '"]').trigger(cascadeEvent) }) }) }, this.dependantUpdateInterval) $.each(toRefresh.fields, function(index, field) { fieldElements.filter('[data-field-name="'+field+'"]:visible') .addClass('loading-indicator-container size-form-field') .loadIndicator() }) } /* * Render tab form fields once a lazy tab is selected. */ FormWidget.prototype.bindLazyTabs = function() { var tabControl = $('[data-control=tab]', this.$el), tabContainer = $('.nav-tabs', tabControl) tabContainer.on('click', '.tab-lazy [data-toggle="tab"]', function() { var $el = $(this), handlerName = $el.data('tab-lazy-handler') $.request(handlerName, { data: { target: $el.data('target'), name: $el.data('tab-name'), section: $el.data('tab-section'), }, success: function(data) { this.success(data) $el.parent().removeClass('tab-lazy') // Trigger all input presets to populate new fields. setTimeout(function() { $('[data-input-preset]').each(function() { var preset = $(this).data('oc.inputPreset') if (preset && preset.$src) { preset.$src.trigger('input') } }) }, 0) } }) }) // If initial active tab is lazy loaded, load it immediately if ($('> li.active.tab-lazy', tabContainer).length) { $('> li.active.tab-lazy > [data-toggle="tab"]', tabContainer).trigger('click') } } /* * Unbind the lazy tab handlers */ FormWidget.prototype.unbindLazyTabs = function() { var tabControl = $('[data-control=tab]', this.$el) $('.nav-tabs', tabControl).off('click', '.tab-lazy [data-toggle="tab"]') } /* * Hides tabs that have no content, it is possible this can be * called multiple times in a single cycle due to input.trigger. */ FormWidget.prototype.toggleEmptyTabs = function() { var self = this, form = this.$el if (this.toggleEmptyTabsTimer !== undefined) { window.clearTimeout(this.toggleEmptyTabsTimer) } this.toggleEmptyTabsTimer = window.setTimeout(function() { var tabControl = $('[data-control=tab]', self.$el), tabContainer = $('.nav-tabs', tabControl) if (!tabControl.length || !form || !form.length || !$.contains(form.get(0), tabControl.get(0))) return /* * Check each tab pane for form field groups */ $('.tab-pane:not(.lazy)', tabControl).each(function() { $('[data-target="#' + $(this).attr('id') + '"]', tabControl) .closest('li') .toggle(!!$('> .form-group:not(:empty):not(.hide)', $(this)).length) }) /* * If a hidden tab was selected, select the first visible tab */ if (!$('> li.active:visible', tabContainer).length) { $('> li:visible:first', tabContainer) .find('> a:first') .tab('show') } }, 1) } /* * Makes sections collapsible by targeting every field after * up until the next section */ FormWidget.prototype.bindCollapsibleSections = function() { $('.section-field[data-field-collapsible]', this.$form) .addClass('collapsed') .find('.field-section:first') .addClass('is-collapsible') .end() .on('click', function() { $(this) .toggleClass('collapsed') .nextUntil('.section-field').toggle() }) .nextUntil('.section-field').hide() } /* * Unbinds collapsible section handlers */ FormWidget.prototype.unbindCollapsibleSections = function() { $('.section-field[data-field-collapsible]', this.$form).off('click') } FormWidget.DEFAULTS = { refreshHandler: null, refreshData: {} } // FORM WIDGET PLUGIN DEFINITION // ============================ var old = $.fn.formWidget $.fn.formWidget = function (option) { var args = arguments, result this.each(function () { var $this = $(this) var data = $this.data('oc.formwidget') var options = $.extend({}, FormWidget.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('oc.formwidget', (data = new FormWidget(this, options))) if (typeof option == 'string') result = data[option].call($this) if (typeof result != 'undefined') return false }) return result ? result : this } $.fn.formWidget.Constructor = FormWidget // FORM WIDGET NO CONFLICT // ================= $.fn.formWidget.noConflict = function () { $.fn.formWidget = old return this } // FORM WIDGET DATA-API // ============== function paramToObj(name, value) { if (value === undefined) value = '' if (typeof value == 'object') return value try { return ocJSON("{" + value + "}") } catch (e) { throw new Error('Error parsing the '+name+' attribute value. '+e) } } $(document).render(function() { $('[data-control="formwidget"]').formWidget(); }) }(window.jQuery);